├── .dockerignore
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── codecov.yml
├── renovate.json
└── workflows
│ ├── ci.yml
│ ├── lint.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── crates
├── archlinux
│ ├── Cargo.toml
│ ├── sample
│ │ └── archlinux.json
│ └── src
│ │ ├── errors
│ │ └── mod.rs
│ │ ├── lib.rs
│ │ ├── response
│ │ ├── external.rs
│ │ ├── internal.rs
│ │ └── mod.rs
│ │ └── test
│ │ └── mod.rs
└── mirro-rs
│ ├── Cargo.toml
│ ├── build.rs
│ ├── completions
│ ├── _mirro-rs
│ ├── mirro-rs.bash
│ ├── mirro-rs.elv
│ └── mirro-rs.fish
│ ├── man
│ └── mirro-rs.1
│ └── src
│ ├── cli
│ └── mod.rs
│ ├── config
│ ├── file.rs
│ ├── mod.rs
│ └── watch.rs
│ ├── dbg
│ └── mod.rs
│ ├── direct
│ └── mod.rs
│ ├── main.rs
│ ├── test
│ └── mod.rs
│ └── tui
│ ├── actions.rs
│ ├── inputs
│ ├── event.rs
│ ├── key.rs
│ └── mod.rs
│ ├── io
│ ├── handler.rs
│ └── mod.rs
│ ├── mod.rs
│ ├── state.rs
│ ├── ui.rs
│ └── view
│ ├── filter.rs
│ ├── mod.rs
│ └── sort.rs
├── examples
├── mirro-rs.json
├── mirro-rs.toml
└── mirro-rs.yaml
└── systemd
├── mirro-rs.service
└── mirro-rs.timer
/.dockerignore:
--------------------------------------------------------------------------------
1 | target
2 | crates/*/Cargo.lock
3 | Dockerfile
4 | .dockerignore
5 | .git
6 | .github
7 | .gitignore
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Toggle '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. Ubuntu 23.10]
28 | - Terminal: [e.g Alacritty]
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: 'enhancement'
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | # ref: https://docs.codecov.com/docs/codecovyml-reference
2 | coverage:
3 | range: 85..100
4 | round: down
5 | precision: 1
6 | status:
7 | project:
8 | default:
9 | threshold: 1%
10 |
11 | comment:
12 | layout: "files"
13 | require_changes: yes
14 |
15 | ignore:
16 | - "**/src/test"
17 | - "**/src/errors"
18 | - "crates/mirro-rs/src/tui"
19 | - "crates/mirro-rs/src/cli"
20 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ],
6 | "semanticCommits": "enabled",
7 | "rebaseWhen": "conflicted",
8 | "packageRules": [
9 | {
10 | "matchPackagePatterns": [
11 | "clap"
12 | ],
13 | "groupName": "clap"
14 | },
15 | {
16 | "matchPackagePatterns": [
17 | "tracing"
18 | ],
19 | "groupName": "tracing",
20 | "automerge": true
21 | },
22 | {
23 | "matchPackagePatterns": [
24 | "anyhow",
25 | "thiserror"
26 | ],
27 | "automerge": true,
28 | "groupName": "error-handling"
29 | },
30 | {
31 | "groupName": "tokio",
32 | "automerge": true,
33 | "matchPackagePatterns": [
34 | "tokio"
35 | ],
36 | "matchCurrentVersion": "!/^0/",
37 | "matchUpdateTypes": [
38 | "patch",
39 | "minor"
40 | ]
41 | },
42 | {
43 | "groupName": "tui",
44 | "matchPackagePatterns": [
45 | "ratatui",
46 | "tui-logger"
47 | ]
48 | },
49 | {
50 | "groupName": "serde",
51 | "automerge": true,
52 | "matchPackagePatterns": [
53 | "^serde"
54 | ],
55 | "matchCurrentVersion": "!/^0/",
56 | "matchUpdateTypes": [
57 | "patch",
58 | "minor"
59 | ]
60 | },
61 | {
62 | "matchUpdateTypes": [
63 | "minor",
64 | "patch"
65 | ],
66 | "matchCurrentVersion": "!/^0/",
67 | "automerge": true
68 | }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [master]
4 | pull_request:
5 | name: rust
6 |
7 | # cancel on going checks when new code is pushed
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
10 | cancel-in-progress: true
11 |
12 | env:
13 | CARGO_INCREMENTAL: 0
14 | CARGO_TERM_COLOR: always
15 |
16 | jobs:
17 | check:
18 | runs-on: ubuntu-latest
19 | name: ubuntu / stable / check
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Install stable
23 | uses: dtolnay/rust-toolchain@stable
24 | - name: cargo generate-lockfile
25 | if: hashFiles('Cargo.lock') == ''
26 | run: cargo generate-lockfile
27 | - name: cargo check
28 | run: cargo check
29 |
30 | hack:
31 | runs-on: ubuntu-latest
32 | name: ubuntu / stable / features
33 | steps:
34 | - uses: actions/checkout@v4
35 | with:
36 | submodules: true
37 | - name: Install stable
38 | uses: dtolnay/rust-toolchain@stable
39 | - name: cargo install cargo-hack
40 | uses: taiki-e/install-action@cargo-hack
41 | - name: cargo hack
42 | run: cargo hack --feature-powerset check --all
43 |
44 | doc:
45 | runs-on: ubuntu-latest
46 | name: nightly / doc
47 | steps:
48 | - uses: actions/checkout@v4
49 | with:
50 | submodules: true
51 | - name: Install nightly
52 | uses: dtolnay/rust-toolchain@nightly
53 | - name: cargo doc
54 | run: cargo doc --no-deps --all-features
55 | env:
56 | RUSTDOCFLAGS: --cfg docsrs
57 |
58 | msrv:
59 | runs-on: ubuntu-latest
60 | strategy:
61 | matrix:
62 | msrv: ["1.74.0"] # clap_mangen
63 | name: ubuntu / ${{ matrix.msrv }}
64 | steps:
65 | - uses: actions/checkout@v4
66 | with:
67 | submodules: true
68 | - name: Install ${{ matrix.msrv }}
69 | uses: dtolnay/rust-toolchain@stable
70 | with:
71 | toolchain: ${{ matrix.msrv }}
72 | - name: cargo +${{ matrix.msrv }} check
73 | run: cargo check --all-features
74 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [master]
4 | pull_request:
5 | name: lint
6 |
7 | # cancel on going checks when new code is pushed
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
10 | cancel-in-progress: true
11 |
12 | env:
13 | CARGO_INCREMENTAL: 0
14 | CARGO_TERM_COLOR: always
15 |
16 | jobs:
17 | fmt:
18 | runs-on: ubuntu-latest
19 | name: stable / fmt
20 | steps:
21 | - uses: actions/checkout@v4
22 | with:
23 | submodules: true
24 | - name: Install stable
25 | uses: dtolnay/rust-toolchain@stable
26 | with:
27 | components: rustfmt
28 | - name: cargo fmt --check
29 | run: cargo fmt --check
30 |
31 | clippy:
32 | runs-on: ubuntu-latest
33 | name: ${{ matrix.toolchain }} / clippy
34 | permissions:
35 | contents: read
36 | checks: write
37 | strategy:
38 | fail-fast: false
39 | matrix:
40 | toolchain: [stable, beta]
41 | steps:
42 | - uses: actions/checkout@v4
43 | with:
44 | submodules: true
45 | - name: Install ${{ matrix.toolchain }}
46 | uses: dtolnay/rust-toolchain@master
47 | with:
48 | toolchain: ${{ matrix.toolchain }}
49 | components: clippy
50 | - name: cargo clippy
51 | uses: giraffate/clippy-action@v1
52 | with:
53 | reporter: 'github-pr-check'
54 | fail_on_error: true
55 | clippy_flags: --all-targets --all-features
56 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/SpectralOps/rust-ci-release-template
2 | name: Release
3 | on:
4 | # schedule:
5 | # - cron: '0 0 * * *' # midnight UTC
6 |
7 | push:
8 | tags:
9 | - 'v[0-9]+.[0-9]+.[0-9]+'
10 | ## - release
11 |
12 | env:
13 | BIN_NAME: mirro-rs
14 | PROJECT_NAME: mirro-rs
15 | REPO_NAME: rtkay123/mirro-rs
16 |
17 | jobs:
18 | dist:
19 | name: Dist
20 | runs-on: ${{ matrix.os }}
21 | strategy:
22 | fail-fast: false # don't fail other jobs if one fails
23 | matrix:
24 | build: [x86_64-linux, x86_64-macos, x86_64-windows, aarch64-linux] #, x86_64-win-gnu, win32-msvc
25 | include:
26 | - build: x86_64-linux
27 | os: ubuntu-20.04
28 | rust: stable
29 | target: x86_64-unknown-linux-gnu
30 | cross: false
31 | - build: x86_64-macos
32 | os: macos-latest
33 | rust: stable
34 | target: x86_64-apple-darwin
35 | cross: false
36 | - build: x86_64-windows
37 | os: windows-2019
38 | rust: stable
39 | target: x86_64-pc-windows-msvc
40 | cross: false
41 | - build: aarch64-linux
42 | os: ubuntu-20.04
43 | rust: stable
44 | target: aarch64-unknown-linux-gnu
45 | cross: true
46 |
47 | steps:
48 | - name: Checkout sources
49 | uses: actions/checkout@v4
50 | with:
51 | submodules: true
52 |
53 | - name: Install ${{ matrix.rust }} toolchain
54 | uses: actions-rs/toolchain@v1
55 | with:
56 | profile: minimal
57 | toolchain: ${{ matrix.rust }}
58 | target: ${{ matrix.target }}
59 | override: true
60 |
61 | - name: Run cargo test
62 | uses: actions-rs/cargo@v1
63 | with:
64 | use-cross: ${{ matrix.cross }}
65 | command: test
66 | args: --release --all-features --locked --target ${{ matrix.target }}
67 |
68 | - name: Build release binary
69 | uses: actions-rs/cargo@v1
70 | with:
71 | use-cross: ${{ matrix.cross }}
72 | command: build
73 | args: --release --all-features --locked --target ${{ matrix.target }}
74 |
75 | - name: Strip release binary (linux and macos)
76 | if: matrix.build == 'x86_64-linux' || matrix.build == 'x86_64-macos'
77 | run: strip "target/${{ matrix.target }}/release/$BIN_NAME"
78 |
79 | - name: Build archive
80 | shell: bash
81 | run: |
82 | mkdir dist
83 | if [ "${{ matrix.os }}" = "windows-2019" ]; then
84 | cp "target/${{ matrix.target }}/release/$BIN_NAME.exe" "dist/"
85 | else
86 | cp "target/${{ matrix.target }}/release/$BIN_NAME" "dist/"
87 | fi
88 |
89 | - uses: actions/upload-artifact@v4.5.0
90 | with:
91 | name: bins-${{ matrix.build }}
92 | path: dist
93 |
94 | publish:
95 | name: Publish
96 | needs: [dist]
97 | runs-on: ubuntu-latest
98 | steps:
99 | - name: Checkout sources
100 | uses: actions/checkout@v4
101 | with:
102 | submodules: false
103 |
104 | - uses: actions/download-artifact@v4
105 | # with:
106 | # path: dist
107 | # - run: ls -al ./dist
108 | - run: ls -al bins-*
109 |
110 | - name: Calculate tag name
111 | run: |
112 | name=dev
113 | if [[ $GITHUB_REF == refs/tags/v* ]]; then
114 | name=${GITHUB_REF:10}
115 | fi
116 | echo ::set-output name=val::$name
117 | echo TAG=$name >> $GITHUB_ENV
118 | id: tagname
119 |
120 | - name: Build archive
121 | shell: bash
122 | run: |
123 | set -ex
124 |
125 | rm -rf tmp
126 | mkdir tmp
127 | mkdir dist
128 |
129 | for dir in bins-* ; do
130 | platform=${dir#"bins-"}
131 | unset exe
132 | if [[ $platform =~ "windows" ]]; then
133 | exe=".exe"
134 | fi
135 | pkgname=$PROJECT_NAME-$TAG-$platform
136 | mkdir tmp/$pkgname
137 | # cp LICENSE README.md tmp/$pkgname
138 | cp LICENSE-MIT LICENSE-APACHE tmp/$pkgname
139 | cp -r crates/mirro-rs/completions tmp/$pkgname
140 | cp -r crates/mirro-rs/man tmp/$pkgname
141 | cp -r examples tmp/$pkgname
142 | cp -r systemd tmp/$pkgname
143 | mv bins-$platform/$BIN_NAME$exe tmp/$pkgname
144 | chmod +x tmp/$pkgname/$BIN_NAME$exe
145 |
146 | if [ "$exe" = "" ]; then
147 | tar cJf dist/$pkgname.tar.xz -C tmp $pkgname
148 | else
149 | (cd tmp && 7z a -r ../dist/$pkgname.zip $pkgname)
150 | fi
151 | done
152 |
153 | - name: Upload binaries to release
154 | uses: svenstaro/upload-release-action@v2
155 | with:
156 | repo_token: ${{ secrets.GITHUB_TOKEN }}
157 | file: dist/*
158 | file_glob: true
159 | tag: ${{ steps.tagname.outputs.val }}
160 | overwrite: true
161 |
162 | - name: Extract version
163 | id: extract-version
164 | run: |
165 | printf "::set-output name=%s::%s\n" tag-name "${GITHUB_REF#refs/tags/}"
166 |
167 | - name: Install stable
168 | uses: dtolnay/rust-toolchain@stable
169 | - run: cargo publish -p ${BIN_NAME} --token ${CRATES_TOKEN}
170 | env:
171 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}
172 |
173 | changelog:
174 | runs-on: ubuntu-latest
175 | needs: [publish]
176 | steps:
177 | - name: Checkout Code
178 | uses: actions/checkout@v4
179 |
180 | - name: Update CHANGELOG
181 | id: changelog
182 | uses: requarks/changelog-action@v1
183 | with:
184 | token: ${{ github.token }}
185 | tag: ${{ github.ref_name }}
186 | useGitmojis: false
187 |
188 | - name: Create Release
189 | uses: ncipollo/release-action@v1.14.0
190 | with:
191 | allowUpdates: true
192 | draft: false
193 | makeLatest: true
194 | name: ${{ github.ref_name }}
195 | body: ${{ steps.changelog.outputs.changes }}
196 | token: ${{ github.token }}
197 |
198 | - name: Commit CHANGELOG.md
199 | uses: stefanzweifel/git-auto-commit-action@v5
200 | with:
201 | branch: master
202 | commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }}'
203 | file_pattern: CHANGELOG.md
204 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: [master]
4 | pull_request:
5 | name: test
6 |
7 | # cancel on going checks when new code is pushed
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
10 | cancel-in-progress: true
11 |
12 | env:
13 | CARGO_INCREMENTAL: 0
14 | CARGO_TERM_COLOR: always
15 |
16 | jobs:
17 | required:
18 | runs-on: ubuntu-latest
19 | name: ubuntu / ${{ matrix.toolchain }}
20 | strategy:
21 | matrix:
22 | toolchain: [stable, beta]
23 | steps:
24 | - uses: actions/checkout@v4
25 | with:
26 | submodules: true
27 | - name: Install ${{ matrix.toolchain }}
28 | uses: dtolnay/rust-toolchain@master
29 | with:
30 | toolchain: ${{ matrix.toolchain }}
31 | - name: cargo generate-lockfile
32 | if: hashFiles('Cargo.lock') == ''
33 | run: cargo generate-lockfile
34 | - name: cargo test --locked
35 | run: cargo test --locked --all-features --all-targets
36 | - name: cargo test --doc
37 | run: cargo test --locked --all-features --doc
38 |
39 | os-check:
40 | runs-on: ${{ matrix.os }}
41 | name: ${{ matrix.os }} / stable
42 | strategy:
43 | fail-fast: false
44 | matrix:
45 | os: [macos-latest, windows-latest]
46 | steps:
47 | - uses: actions/checkout@v4
48 | - name: Install stable
49 | uses: dtolnay/rust-toolchain@stable
50 | - name: cargo generate-lockfile
51 | if: hashFiles('Cargo.lock') == ''
52 | run: cargo generate-lockfile
53 | - name: cargo test --workspace
54 | run: cargo test --no-run --workspace --locked --all-features --all-targets
55 |
56 | coverage:
57 | runs-on: ubuntu-latest
58 | name: ubuntu / stable / coverage
59 | steps:
60 | - uses: actions/checkout@v4
61 | - name: Install stable
62 | uses: dtolnay/rust-toolchain@stable
63 | with:
64 | components: llvm-tools-preview
65 | - name: cargo install cargo-llvm-cov
66 | uses: taiki-e/install-action@cargo-llvm-cov
67 | - name: cargo llvm-cov
68 | run: cargo llvm-cov --workspace --locked --all-features --lcov --output-path lcov.info
69 | - name: Upload to codecov.io
70 | uses: codecov/codecov-action@v4
71 | with:
72 | fail_ci_if_error: true
73 | token: ${{ secrets.CODECOV_TOKEN }}
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | crates/*/Cargo.lock
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [v0.2.3] - 2024-03-02
2 | ### Bug Fixes
3 | - [`db93756`](https://github.com/rtkay123/mirro-rs/commit/db93756fc2ed74a944be81dac70d5a72263ec29d) - nightly docs builds
4 | - [`727f570`](https://github.com/rtkay123/mirro-rs/commit/727f5709265d5309d3846bbaeba7f6cf5bd79118) - **deps**: update rust crate chrono to 0.4.34 *(commit by @renovate[bot])*
5 | - [`07e3c72`](https://github.com/rtkay123/mirro-rs/commit/07e3c72f9fc8f9a6bd4b1756c62a594809565e3b) - **deps**: update rust crate unicode-width to 0.1.11 *(commit by @renovate[bot])*
6 | - [`b359df8`](https://github.com/rtkay123/mirro-rs/commit/b359df8205a20b4e86b636e1c4d8a9949746d473) - **deps**: update rust crate log to 0.4.20 *(commit by @renovate[bot])*
7 | - [`3c3f33e`](https://github.com/rtkay123/mirro-rs/commit/3c3f33eb35c39e4e81d41571f337358caf4cd4a9) - **deps**: update tui *(commit by @renovate[bot])*
8 | - [`b73451e`](https://github.com/rtkay123/mirro-rs/commit/b73451ed8d7b4653cba9f541fc6daf55e3b8ca05) - **deps**: update rust crate reqwest to 0.11.24 *(commit by @renovate[bot])*
9 | - [`f544b57`](https://github.com/rtkay123/mirro-rs/commit/f544b57de842da4ee2a75f4a41b570db86e63918) - **deps**: update rust crate ahash to 0.8.8 *(commit by @renovate[bot])*
10 | - [`d24c337`](https://github.com/rtkay123/mirro-rs/commit/d24c33794f2369cfeb1cabbf3380a2203c373e23) - **deps**: update rust crate thiserror to 1.0.57 *(commit by @renovate[bot])*
11 | - [`3221ed7`](https://github.com/rtkay123/mirro-rs/commit/3221ed7967954416140b68b40fd5e30f506d1754) - **deps**: update rust crate ratatui to 0.26.1 *(commit by @renovate[bot])*
12 | - [`958d9e6`](https://github.com/rtkay123/mirro-rs/commit/958d9e6e246e2c6eb042e065f7f5300b63bb765a) - **deps**: update rust crate anyhow to 1.0.80 *(commit by @renovate[bot])*
13 | - [`083b93d`](https://github.com/rtkay123/mirro-rs/commit/083b93dbc6a5065fdd9c668b38845548fec1e898) - **deps**: update rust crate serde_yaml to 0.9.32 *(commit by @renovate[bot])*
14 | - [`a304aa4`](https://github.com/rtkay123/mirro-rs/commit/a304aa456c557bf31cf51da4250942da0cb96454) - **deps**: update rust crate ahash to 0.8.9 *(commit by @renovate[bot])*
15 | - [`a471dca`](https://github.com/rtkay123/mirro-rs/commit/a471dca192db41aa8b4de33308fd480880ae411a) - **deps**: update rust crate ahash to 0.8.10 *(commit by @renovate[bot])*
16 | - [`62895cb`](https://github.com/rtkay123/mirro-rs/commit/62895cb6c7cbe15302bd660e17dec38308b180db) - **deps**: update rust crate log to 0.4.21 *(commit by @renovate[bot])*
17 |
18 | ### Chores
19 | - [`cb51fef`](https://github.com/rtkay123/mirro-rs/commit/cb51fef5d619510914609a9e11023653a6e6cdfb) - update deps
20 | - [`79046b6`](https://github.com/rtkay123/mirro-rs/commit/79046b6b17d5bc6534eb12d4a1848482277e342c) - use workspace deps *(PR #90 by @rtkay123)*
21 | - [`cb9faed`](https://github.com/rtkay123/mirro-rs/commit/cb9faedb4b8ca64c749ed02d8081d6af72c960b9) - update config location
22 | - [`182c4ca`](https://github.com/rtkay123/mirro-rs/commit/182c4ca8fc1a76f3d45e16d4e69a71d31110cdc7) - update config location
23 | - [`d284aad`](https://github.com/rtkay123/mirro-rs/commit/d284aad7b09b753e37a34a4370aec255e3656319) - **deps**: update actions/upload-artifact action to v4.3.1 *(commit by @renovate[bot])*
24 | - [`ea56bc0`](https://github.com/rtkay123/mirro-rs/commit/ea56bc0fe8c9f9e708107745198e71c10218d0d2) - **deps**: update rust crate clap_mangen to 0.2.20 *(commit by @renovate[bot])*
25 | - [`132e725`](https://github.com/rtkay123/mirro-rs/commit/132e725936c5bd39ad8f5c27804ca6b18f60b772) - update msrv badge
26 | - [`0528c35`](https://github.com/rtkay123/mirro-rs/commit/0528c359926f393541f7fb0c67add516e8c8a7a2) - **deps**: update clap to 4.5.0 *(commit by @renovate[bot])*
27 | - [`d86c6da`](https://github.com/rtkay123/mirro-rs/commit/d86c6da49417c2a05e3e2e30ce75b3e6801ee706) - **deps**: update ncipollo/release-action action to v1.14.0 *(commit by @renovate[bot])*
28 | - [`18659e1`](https://github.com/rtkay123/mirro-rs/commit/18659e12223d7f4807b0c966616cc4b90e224052) - **deps**: update rust crate itertools to 0.12.1 *(commit by @renovate[bot])*
29 | - [`8c36740`](https://github.com/rtkay123/mirro-rs/commit/8c367400616a30c953e3cdfa81976ae3adf98851) - **deps**: update alpine docker tag to v3.19.1 *(commit by @renovate[bot])*
30 | - [`b9eef3f`](https://github.com/rtkay123/mirro-rs/commit/b9eef3fc4ec2762443edd0e46a2479f87196929f) - **deps**: update rust crate toml to 0.8.10 *(commit by @renovate[bot])*
31 | - [`81ddbdd`](https://github.com/rtkay123/mirro-rs/commit/81ddbdde61928640423dd5a13d4ba35e75be8eb4) - **deps**: update rust crate tokio to 1.36.0 *(commit by @renovate[bot])*
32 | - [`1ffd1fc`](https://github.com/rtkay123/mirro-rs/commit/1ffd1fcbd34ac859ef29f7fab0102f497c6f5a79) - **deps**: update rust crate clap to 4.5.1 *(commit by @renovate[bot])*
33 | - [`8180f94`](https://github.com/rtkay123/mirro-rs/commit/8180f9419a15ee55b6fb6d99a6dafeff3ad4e5a8) - **deps**: update rust crate clap_complete to 4.5.1 *(commit by @renovate[bot])*
34 | - [`c83e0ba`](https://github.com/rtkay123/mirro-rs/commit/c83e0ba9fae562277d17250fd03eb2b826c3c071) - **deps**: update rust crate serde to 1.0.197 *(commit by @renovate[bot])*
35 | - [`b02a39c`](https://github.com/rtkay123/mirro-rs/commit/b02a39cef3822bbb6b76a5c2c370d34e233e7643) - **deps**: update rust crate serde_json to 1.0.114 *(commit by @renovate[bot])*
36 | - [`26c6966`](https://github.com/rtkay123/mirro-rs/commit/26c6966d44909ed6e8bab79f16689895c9d5c6a3) - bump pkg ver
37 |
38 | ## [v0.2.2] - 2023-11-19
39 | ### New Features
40 | - [`37245a4`](https://github.com/rtkay123/mirro-rs/commit/37245a4139436c694967ca9aa3a1941f025958af) - get mirrors with client
41 | - [`25250b0`](https://github.com/rtkay123/mirro-rs/commit/25250b0bac3746e984cea3c1047e6a24e5c84506) - replace logger with tracing
42 | - [`6506dd1`](https://github.com/rtkay123/mirro-rs/commit/6506dd1ea2f45c66d0029e000d619ea72a522468) - handle cases where journald is absent
43 |
44 | ### Bug Fixes
45 | - [`d829437`](https://github.com/rtkay123/mirro-rs/commit/d829437e966990a10dd86869fd1753f63f232b61) - remove flaky parsing and get lastsync timestamp directly from url *(commit by [@phanen](https://github.com/phanen))*
46 | - [`6a4c8ac`](https://github.com/rtkay123/mirro-rs/commit/6a4c8acd1e2f9a559a28c0325beb66747975cfc8) - skip journal errors outside of unix
47 |
48 | ### Refactors
49 | - [`983bc38`](https://github.com/rtkay123/mirro-rs/commit/983bc38208406eeaf672f1887d1f55143ac684b2) - use non blocking fs module
50 |
51 | ### Chores
52 | - [`8f610b2`](https://github.com/rtkay123/mirro-rs/commit/8f610b2247bbe76191c8199071767de6e2fdb357) - bump lib ver
53 | - [`0306ff3`](https://github.com/rtkay123/mirro-rs/commit/0306ff300b4564d0626103a8aa3454da31021d98) - update changelog
54 | - [`b31531c`](https://github.com/rtkay123/mirro-rs/commit/b31531c2510146ba2f7a92f09ac4edf3c40813e6) - bump pkg ver
55 | - [`9b3b56a`](https://github.com/rtkay123/mirro-rs/commit/9b3b56a9d1ff83da121bec55d4930fcf95a22c15) - clippy fix
56 | - [`bfd2b08`](https://github.com/rtkay123/mirro-rs/commit/bfd2b0825cc0500ccdd67c01bbbb1f5cb629be35) - remove log comments
57 |
58 |
59 | ## [v0.2.1] - 2023-11-14
60 |
61 | ### What's Changed
62 | * tests: add more tests to test suite by @rtkay123 in https://github.com/rtkay123/mirro-rs/pull/22
63 | * Implement From to convert args to config struct by @rtkay123 in https://github.com/rtkay123/mirro-rs/pull/23
64 | * docs: update README.md about official Arch Linux package by @orhun in https://github.com/rtkay123/mirro-rs/pull/24
65 |
66 | ### New Contributors
67 | * @dependabot made their first contribution in https://github.com/rtkay123/mirro-rs/pull/15
68 | * @orhun made their first contribution in https://github.com/rtkay123/mirro-rs/pull/24
69 |
70 | **Full Changelog**: https://github.com/rtkay123/mirro-rs/compare/v0.2.0...v0.2.1
71 |
72 | ## [v0.2.0] - 2023-11-11
73 |
74 | ### What's Changed
75 | * fix: make `ftp` known as protocol type by @rtkay123 in https://github.com/rtkay123/mirro-rs/pull/8
76 | * chore: replace tui with ratatui by @rtkay123 in https://github.com/rtkay123/mirro-rs/pull/9
77 | * refactor: replace hyper with reqwest by @rtkay123 in https://github.com/rtkay123/mirro-rs/pull/10
78 |
79 | ### New Contributors
80 | * @rtkay123 made their first contribution in https://github.com/rtkay123/mirro-rs/pull/8
81 |
82 | [v0.2.2]: https://github.com/rtkay123/mirro-rs/compare/v0.2.1...v0.2.2
83 | [v0.2.3]: https://github.com/rtkay123/mirro-rs/compare/v0.2.2...v0.2.3
84 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [ "crates/*" ]
3 | resolver = "2"
4 |
5 | [workspace.dependencies]
6 | itertools = "0.13.0"
7 | serde = "1.0.200"
8 | serde_json = "1.0.116"
9 | tokio = "1.37.0"
10 |
11 | [profile.release]
12 | panic = "abort"
13 | lto = true
14 | strip = true
15 | codegen-units = 1
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.21.0
2 |
3 | ENV RUSTFLAGS="-C target-feature=-crt-static"
4 |
5 | RUN apk add --no-cache gcc musl-dev rustup
6 |
7 | RUN rustup-init -t x86_64-unknown-linux-musl --default-toolchain nightly --profile minimal -y
8 |
9 | WORKDIR /usr/src/app
10 |
11 | COPY . .
12 |
13 | RUN /root/.cargo/bin/cargo build --release --all-features
14 |
15 | FROM alpine:3.21.0
16 |
17 | RUN apk add --no-cache libgcc
18 |
19 | COPY --from=0 /usr/src/app/target/release/mirro-rs /bin/
20 |
21 | ENTRYPOINT ["mirro-rs"]
22 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 kawaki-san
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
7 | mirro-rs
8 |
9 | A mirrorlist manager for Arch Linux systems
10 |
11 | View usage examples »
12 |
13 |
14 | Report Bug
15 | ·
16 | Request Feature
17 |
18 |
19 | 
20 |
21 | mirro-rs provides a TUI to help you better visualise managing your mirrorlist.
22 |
23 | ## Features
24 |
25 | - Sorting
26 | - Completion - The number of mirror checks (as a percentage) that have successfully connected and disconnected from the given URL. If this is below 100%, the mirror may be unreliable.
27 | - Score - It is currently calculated as (hours delay + average duration + standard deviation) / completion percentage. _Lower is better_.
28 | - Standard deviation - The standard deviation of the connect and retrieval time. A high standard deviation can indicate an unstable or overloaded mirror.
29 | - Delay - The mean value of last check − last sync for each check of this mirror URL. Due to the timing of mirror checks, any value under one hour should be viewed as ideal.
30 | - Rate - sort by download speed
31 | - Filtering
32 | - Age
33 | - Country
34 | - ipv4, ipv6, isos
35 | - Protocol - `http`, `https`, `ftp` or `rsync`
36 | - Completion Percentage
37 |
38 | ## Getting Started
39 |
40 | ### Installation
41 |
42 | Install from the Arch Linux official repository:
43 |
44 | ```sh
45 | pacman -S mirro-rs
46 | ```
47 |
48 | `mirro-rs` is also available in the AUR. If you're using `paru`:
49 |
50 | ```sh
51 | paru -S mirro-rs-git
52 | ```
53 |
54 | > **Note**
55 | > By default, this enables [configuration](#configuration) through `toml` files. You should edit the `PKGBUILD` if you prefer another configuration format (or to disable configuration files altogether).
56 |
57 | ### Manual Compilation
58 |
59 | - cargo
60 |
61 | You need to have `cargo` installed to build the application. The easiest way to set this up is installing `rustup`.
62 |
63 | ```sh
64 | pacman -S rustup
65 | ```
66 |
67 | Install a rust toolchain:
68 |
69 | ```sh
70 | rustup install stable
71 | ```
72 |
73 | - git
74 |
75 | Clone the repository:
76 |
77 | ```sh
78 | git clone https://github.com/rtkay123/mirro-rs
79 | ```
80 |
81 | You may then build the release target:
82 |
83 | ```sh
84 | cargo build --release
85 | ```
86 |
87 | ### Usage
88 |
89 | Pass the `-h` or `--help` flag to mirro-rs to view configuration parameters.
90 | To preview `http` or `https` mirrors that were successfully synchronised in the last 24 hours and use `/home/user/mirrorlist` as an export location for the best (at max) 50:
91 |
92 | ```sh
93 | mirro-rs --export 50 --protocols https --protocols http --age 24 --outfile "/home/user/mirrorlist"
94 | ```
95 |
96 | To do the same but restrict the sources to be from France and the UK:
97 |
98 | ```sh
99 | mirro-rs --export 50 --protocols https --protocols http --age 24 --outfile "/home/user/mirrorlist" -c France -c "United Kingdom"
100 | ```
101 |
102 | #### Configuration
103 |
104 | For convenience, mirro-rs optionally supports reading a configuration `[default: $XDG_CONFIG_HOME/mirro-rs/mirro-rs.toml]` for general preferences. If none is available, `[default: $XDG_CONFIG_HOME/mirro-rs.toml]` will be used. If both are available, the former takes priority.
105 |
106 | For `toml` support:
107 |
108 | ```sh
109 | cargo build --release --features toml
110 | ```
111 |
112 | For `json` support:
113 |
114 | ```sh
115 | cargo build --release --features json
116 | ```
117 |
118 | Likewise, for `yaml` support:
119 |
120 | ```sh
121 | cargo build --release --features yaml
122 | ```
123 |
124 | > **Note**
125 | > If you enable all configuration file features, if the configuration directory contains more than one valid file format, the order of priority goes from `toml` -> `json` -> `yaml`.
126 |
127 | Sample configuration files are provided in the [examples](examples) folder.
128 |
129 | A minimal `mirro-rs.toml` config file could look like:
130 |
131 | ```toml
132 | cache-ttl = 24
133 | timeout = 10
134 | ```
135 |
136 | > **Note**
137 | > Changing the configuration file at runtime will overwrite the parameters that were set as CLI arguments
138 |
139 | ## License
140 |
141 | Licensed under either of
142 |
143 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://apache.org/licenses/LICENSE-2.0)
144 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
145 |
146 | ### Contribution
147 |
148 | Unless you explicitly state otherwise, any contribution intentionally submitted
149 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall
150 | be dual licensed as above, without any additional terms or conditions.
151 |
--------------------------------------------------------------------------------
/crates/archlinux/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mirrors-arch"
3 | version = "0.1.3"
4 | edition = "2021"
5 | license = "MIT OR Apache-2.0"
6 | authors = ["rtkay123 "]
7 | description = "An ArchLinux mirrorlist retriever used by mirro-rs"
8 | repository = "https://github.com/rtkay123/mirro-rs"
9 | homepage = "https://github.com/rtkay123/mirro-rs"
10 |
11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12 |
13 | [dependencies]
14 | chrono = { version = "0.4.38", features = ["serde"], optional = true }
15 | futures = "0.3.30"
16 | itertools.workspace = true
17 | log = "0.4.21"
18 | reqwest = { version = "0.12.4", default-features = false, features = ["json", "rustls-tls"] }
19 | serde = { workspace = true, features = ["derive"] }
20 | serde_json.workspace = true
21 | thiserror = "1.0.59"
22 |
23 | [dev-dependencies]
24 | tokio = { workspace = true, features = ["macros"] }
25 |
26 | [features]
27 | default = []
28 | time = ["dep:chrono"]
29 |
30 | # docs.rs-specific configuration
31 | [package.metadata.docs.rs]
32 | # document all features
33 | all-features = true
34 | # defines the configuration attribute `docsrs`
35 | rustdoc-args = [
36 | "--cfg",
37 | "docsrs"
38 | ]
39 |
--------------------------------------------------------------------------------
/crates/archlinux/src/errors/mod.rs:
--------------------------------------------------------------------------------
1 | use reqwest::StatusCode;
2 | use thiserror::Error;
3 |
4 | #[derive(Error, Debug)]
5 | /// Error type definitions returned by the crate
6 | pub enum Error {
7 | /// The connection could not be made (perhaps a network error is
8 | /// the cause)
9 | #[error("could not establish connection")]
10 | Connection(#[from] reqwest::Error),
11 | /// The response could not be parsed to an internal type
12 | #[error("could not parse response")]
13 | Parse(#[from] serde_json::Error),
14 | /// The mirror could not be rated
15 | #[error("could not find file (expected {qualified_url:?}, from {url:?}), server returned {status_code:?}")]
16 | Rate {
17 | /// The URL including the filepath that was sent in the request
18 | qualified_url: String,
19 | /// The URL of the particular mirror
20 | url: String,
21 | /// The status code returned by the server
22 | status_code: StatusCode,
23 | },
24 | #[error("could not build request {0}")]
25 | /// There was an error performing the request
26 | Request(String),
27 | /// There was an error performing the request
28 | #[cfg(feature = "time")]
29 | #[error("could not parse time")]
30 | TimeError(#[from] chrono::ParseError),
31 | }
32 |
--------------------------------------------------------------------------------
/crates/archlinux/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(docsrs, feature(doc_cfg))]
2 | #![warn(
3 | missing_docs,
4 | rustdoc::broken_intra_doc_links,
5 | missing_debug_implementations
6 | )]
7 |
8 | //! # mirrors-arch
9 | use std::time::{Duration, Instant};
10 |
11 | use futures::{future::BoxFuture, FutureExt};
12 | use log::{info, trace};
13 | use reqwest::{header::LOCATION, ClientBuilder, Response, StatusCode};
14 |
15 | use crate::response::external::Root;
16 |
17 | #[cfg(test)]
18 | mod test;
19 |
20 | mod errors;
21 | pub use errors::Error;
22 |
23 | pub use reqwest::Client;
24 |
25 | mod response;
26 | #[cfg(feature = "time")]
27 | #[cfg_attr(docsrs, doc(cfg(feature = "time")))]
28 | #[doc(no_inline)]
29 | pub use chrono;
30 |
31 | pub use response::{external::Protocol, internal::*};
32 |
33 | type Result = std::result::Result;
34 |
35 | pub(crate) const FILE_PATH: &str = "core/os/x86_64/core.db.tar.gz";
36 |
37 | /// Get ArchLinux mirrors from an `json` endpoint and return them in a [minified](ArchLinux) format
38 | ///
39 | /// # Parameters
40 | ///
41 | /// - `source` - The URL to query for a mirrorlist
42 | /// - `with_timeout` - Connection timeout (in seconds) to be used in network requests
43 | ///
44 | /// # Example
45 | ///
46 | /// ```rust
47 | /// # use mirrors_arch::get_mirrors;
48 | /// # async fn foo()->Result<(), Box>{
49 | /// let arch_mirrors = get_mirrors("https://archlinux.org/mirrors/status/json/", None).await?;
50 | /// println!("{arch_mirrors:?}");
51 | /// # Ok(())
52 | /// # }
53 | /// ```
54 | pub async fn get_mirrors(source: &str, with_timeout: Option) -> Result {
55 | let response = get_response(source, with_timeout).await?;
56 |
57 | let root: Root = response.json().await?;
58 |
59 | let body = ArchLinux::from(root);
60 | let count = body.countries.len();
61 | info!("located mirrors from {count} countries");
62 | Ok(body)
63 | }
64 |
65 | async fn get_response(source: &str, with_timeout: Option) -> Result {
66 | trace!("creating http client");
67 | let client = get_client(with_timeout)?;
68 |
69 | trace!("sending request");
70 | let response = client.get(source).send().await?;
71 |
72 | Ok(response)
73 | }
74 |
75 | /// The same as [get_mirrors] but returns a tuple including the json as a
76 | /// `String`
77 | ///
78 | /// # Example
79 | ///
80 | /// ```rust
81 | /// # use mirrors_arch::get_mirrors_with_raw;
82 | /// # async fn foo()->Result<(), Box>{
83 | /// let timeout = Some(10);
84 | /// let arch_mirrors = get_mirrors_with_raw("https://my-url.com/json/", timeout).await?;
85 | /// println!("{arch_mirrors:?}");
86 | /// # Ok(())
87 | /// # }
88 | /// ```
89 | pub async fn get_mirrors_with_raw(
90 | source: &str,
91 | with_timeout: Option,
92 | ) -> Result<(ArchLinux, String)> {
93 | let response = get_response(source, with_timeout).await?;
94 | deserialise_mirrors(response).await
95 | }
96 |
97 | async fn deserialise_mirrors(response: Response) -> Result<(ArchLinux, String)> {
98 | let root: Root = response.json().await?;
99 |
100 | let value = serde_json::to_string(&root)?;
101 | Ok((ArchLinux::from(root), value))
102 | }
103 |
104 | /// The same as [get_mirrors_with_raw] but uses a specified
105 | /// [Client] for requests
106 | pub async fn get_mirrors_with_client(source: &str, client: Client) -> Result<(ArchLinux, String)> {
107 | let response = client.get(source).send().await?;
108 | deserialise_mirrors(response).await
109 | }
110 |
111 | /// Parses a `string slice` to the [ArchLinux] type
112 | ///
113 | /// # Parameters
114 | /// - `contents` - A `json` string slice to be parsed and returned as a [mirrorlist](ArchLinux)
115 | ///
116 | /// # Example
117 | ///
118 | /// ```rust
119 | /// # use mirrors_arch::parse_local;
120 | /// # async fn foo()->Result<(), Box>{
121 | /// let json = std::fs::read_to_string("archmirrors.json")?;
122 | /// let arch_mirrors = parse_local(&json)?;
123 | /// println!("{arch_mirrors:?}");
124 | /// # Ok(())
125 | /// # }
126 | /// ```
127 | pub fn parse_local(contents: &str) -> Result {
128 | let vals = ArchLinux::from(serde_json::from_str::(contents)?);
129 | Ok(vals)
130 | }
131 |
132 | /// Gets a client that can be used to rate mirrors
133 | ///
134 | /// # Parameters
135 | /// - `with_timeout` - an optional connection timeout to be used when rating the mirrors
136 | ///
137 | /// # Example
138 | ///
139 | /// ```rust
140 | /// # use mirrors_arch::get_client;
141 | /// # async fn foo()->Result<(), Box>{
142 | /// let timeout = Some(5);
143 | /// let client = get_client(timeout);
144 | /// # Ok(())
145 | /// # }
146 | /// ```
147 | pub fn get_client(with_timeout: Option) -> Result {
148 | let timeout = with_timeout.map(Duration::from_secs);
149 |
150 | let mut client_builder = ClientBuilder::new();
151 | if let Some(timeout) = timeout {
152 | client_builder = client_builder.timeout(timeout).connect_timeout(timeout);
153 | }
154 |
155 | Ok(client_builder.build()?)
156 | }
157 |
158 | /// Queries a mirrorlist and calculates how long it took to get a response
159 | ///
160 | /// # Parameters
161 | /// - `url` - The mirrorlist
162 | /// - `client` - The client returned from [get_client]
163 | ///
164 | /// # Example
165 | ///
166 | /// ```rust
167 | /// # use mirrors_arch::{get_client, rate_mirror};
168 | /// # async fn foo()->Result<(), Box>{
169 | /// # let url = String::default();
170 | /// # let client = get_client(Some(5))?;
171 | /// let (duration, url) = rate_mirror(url, client).await?;
172 | /// # Ok(())
173 | /// # }
174 | /// ```
175 | pub fn rate_mirror(url: String, client: Client) -> BoxFuture<'static, Result<(Duration, String)>> {
176 | async move {
177 | let uri = format!("{url}{FILE_PATH}");
178 |
179 | let now = Instant::now();
180 |
181 | let response = client.get(&uri).send().await?;
182 |
183 | if response.status() == StatusCode::OK {
184 | Ok((now.elapsed(), url))
185 | } else if response.status() == StatusCode::MOVED_PERMANENTLY {
186 | if let Some(new_uri) = response.headers().get(LOCATION) {
187 | let new_url = String::from_utf8_lossy(new_uri.as_bytes()).replace(FILE_PATH, "");
188 | rate_mirror(new_url.to_string(), client.clone()).await
189 | } else {
190 | Err(Error::Rate {
191 | qualified_url: uri,
192 | url,
193 | status_code: response.status(),
194 | })
195 | }
196 | } else {
197 | Err(Error::Rate {
198 | qualified_url: uri,
199 | url,
200 | status_code: response.status(),
201 | })
202 | }
203 | }
204 | .boxed()
205 | }
206 |
207 | /// Gets a mirror's last sync time
208 | /// # Parameters
209 | /// - `mirror` - The mirror to get the last sync time for
210 | /// - `client` - A [reqwest::Client]
211 | ///
212 | /// # Example
213 | ///
214 | /// ```rust
215 | /// # use mirrors_arch::{get_client, get_last_sync};
216 | /// # async fn foo()->Result<(), Box>{
217 | /// # let mirror = String::default();
218 | /// # let client = get_client(Some(5))?;
219 | /// let (date_time, mirror) = get_last_sync(mirror, client).await?;
220 | /// # Ok(())
221 | /// # }
222 | /// ```
223 |
224 | #[cfg(feature = "time")]
225 | #[cfg_attr(docsrs, doc(cfg(feature = "time")))]
226 | pub async fn get_last_sync(
227 | mirror: impl Into,
228 | client: Client,
229 | ) -> Result<(chrono::DateTime, String)> {
230 | let mirror = mirror.into();
231 | let lastsync_url = format!("{mirror}lastsync");
232 |
233 | let timestamp = client
234 | .get(&lastsync_url)
235 | .send()
236 | .await
237 | .map_err(|e| Error::Request(e.to_string()))?
238 | .text()
239 | .await?;
240 |
241 | let result = chrono::NaiveDateTime::parse_from_str(×tamp, "%s")
242 | .map(|res| chrono::DateTime::::from_naive_utc_and_offset(res, chrono::Utc))
243 | .map_err(Error::TimeError)?;
244 |
245 | Ok((result, mirror))
246 | }
247 |
--------------------------------------------------------------------------------
/crates/archlinux/src/response/external.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | #[cfg(feature = "time")]
4 | use chrono::{DateTime, Utc};
5 | use serde::{Deserialize, Serialize};
6 |
7 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
8 | pub(crate) struct Root {
9 | pub cutoff: u32,
10 | #[cfg(feature = "time")]
11 | pub last_check: DateTime,
12 | #[cfg(not(feature = "time"))]
13 | pub last_check: String,
14 | pub num_checks: u8,
15 | pub check_frequency: u16,
16 | pub urls: Vec,
17 | pub version: u8,
18 | }
19 |
20 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
21 | pub(crate) struct Url {
22 | pub url: String,
23 | pub protocol: Protocol,
24 | #[cfg(feature = "time")]
25 | pub last_sync: Option>,
26 | #[cfg(not(feature = "time"))]
27 | pub last_sync: Option,
28 | pub completion_pct: f32,
29 | pub delay: Option,
30 | pub duration_avg: Option,
31 | pub duration_stddev: Option,
32 | pub score: Option,
33 | pub active: bool,
34 | pub country: String,
35 | pub country_code: String,
36 | pub isos: bool,
37 | pub ipv4: bool,
38 | pub ipv6: bool,
39 | pub details: String,
40 | }
41 |
42 | #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
43 | #[serde(rename_all = "lowercase")]
44 | /// Protocols serving the mirrors
45 | pub enum Protocol {
46 | /// rsync
47 | Rsync,
48 | /// http
49 | Http,
50 | /// https
51 | Https,
52 | /// ftp
53 | Ftp,
54 | }
55 |
56 | impl Display for Protocol {
57 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 | write!(
59 | f,
60 | "{}",
61 | match self {
62 | Protocol::Rsync => "rsync",
63 | Protocol::Http => "http",
64 | Protocol::Https => "https",
65 | Protocol::Ftp => "ftp",
66 | }
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/crates/archlinux/src/response/internal.rs:
--------------------------------------------------------------------------------
1 | use itertools::Itertools;
2 | use log::debug;
3 | use serde::Deserialize;
4 |
5 | #[cfg(feature = "time")]
6 | use chrono::{DateTime, Utc};
7 |
8 | use super::external::{Protocol, Root};
9 |
10 | #[derive(Debug, Clone, PartialEq, Deserialize)]
11 | /// The type returned as the mirrorlist
12 | pub struct ArchLinux {
13 | /// Cutoff as returned by the server
14 | pub cutoff: u32,
15 | #[cfg(feature = "time")]
16 | /// Last successful check for mirrorlists
17 | pub last_check: DateTime,
18 | #[cfg(not(feature = "time"))]
19 | /// Last successful check for mirrorlists
20 | pub last_check: String,
21 | /// Number of checks as returned by the server
22 | pub num_checks: u8,
23 | /// Check frequency as returned by the server
24 | pub check_frequency: u16,
25 | /// A list of [countries](Country) that group [mirrors](Mirror)
26 | pub countries: Vec,
27 | /// Version number as returned by the server
28 | pub version: u8,
29 | }
30 |
31 | #[derive(Debug, Clone, PartialEq, Deserialize)]
32 | /// Holds a collection of mirrors
33 | pub struct Country {
34 | /// The string representation of the country name
35 | pub name: String,
36 | /// A short representation the the current country, i.e `ZA` for `South Africa`
37 | pub code: String,
38 | /// A list of [mirrors](Mirror)
39 | pub mirrors: Vec,
40 | }
41 |
42 | #[derive(Debug, Clone, PartialEq, Deserialize)]
43 | /// An ArchLinux mirror
44 | pub struct Mirror {
45 | /// The mirror's URL
46 | pub url: String,
47 | /// Represents a mirror's [protocol](Protocol)
48 | pub protocol: Protocol,
49 | /// The number of mirror checks that have successfully connected and disconnected from the given URL. If it's less than 100, it may be a sign of an unreliable mirror
50 | pub completion_pct: f32,
51 | /// The calculated average mirroring delay; e.g. the mean value of `last_check − last_sync` for each check of this mirror URL. Any value under one hour should be viewed as ideal.
52 | pub delay: Option,
53 | ///A very rough calculation for ranking mirrors. It is currently calculated as `(hours delay + average duration + standard deviation) / completion percentage`. Lower is better.
54 | pub score: Option,
55 | /// The standard deviation of the connect and retrieval time. A high standard deviation can indicate an unstable or overloaded mirror.
56 | pub duration_stddev: Option,
57 | /// Time when the last successful synchronisation occurred
58 | #[cfg(feature = "time")]
59 | pub last_sync: Option>,
60 | #[cfg(not(feature = "time"))]
61 | /// Time when the last successful synchronisation occurred
62 | pub last_sync: Option,
63 | /// ipv4 enabled
64 | pub ipv4: bool,
65 | /// ipv6 enabled
66 | pub ipv6: bool,
67 | /// isos enabled
68 | pub isos: bool,
69 | }
70 |
71 | impl From for ArchLinux {
72 | fn from(mut raw: Root) -> Self {
73 | debug!("minifying mirrors");
74 | raw.urls.sort_by(|a, b| a.country.cmp(&b.country));
75 | let countries = raw
76 | .urls
77 | .iter()
78 | .dedup_by(|a, b| a.country == b.country)
79 | .map(|f| f.country.to_string())
80 | .collect_vec();
81 |
82 | let mut output = Vec::with_capacity(countries.len());
83 | let urls = &raw.urls;
84 |
85 | for i in countries.iter() {
86 | let mut code = String::default();
87 | let mirrors = urls
88 | .iter()
89 | .filter_map(|f| {
90 | if f.country.eq_ignore_ascii_case(i) {
91 | code = f.country_code.clone();
92 | Some(Mirror {
93 | url: f.url.clone(),
94 | protocol: f.protocol,
95 | completion_pct: f.completion_pct,
96 | delay: f.delay,
97 | score: f.score,
98 | duration_stddev: f.duration_stddev,
99 | #[cfg(feature = "time")]
100 | last_sync: f.last_sync,
101 | #[cfg(not(feature = "time"))]
102 | last_sync: f.last_sync.clone(),
103 | ipv4: f.ipv4,
104 | ipv6: f.ipv6,
105 | isos: f.isos,
106 | })
107 | } else {
108 | None
109 | }
110 | })
111 | .collect_vec();
112 | let country = Country {
113 | name: i.to_string(),
114 | code,
115 | mirrors,
116 | };
117 | output.push(country);
118 | }
119 | Self {
120 | cutoff: raw.cutoff,
121 | last_check: raw.last_check,
122 | num_checks: raw.num_checks,
123 | check_frequency: raw.check_frequency,
124 | countries: output,
125 | version: raw.version,
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/crates/archlinux/src/response/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod external;
2 | pub mod internal;
3 |
--------------------------------------------------------------------------------
/crates/archlinux/src/test/mod.rs:
--------------------------------------------------------------------------------
1 | use reqwest::{Response, StatusCode};
2 |
3 | use super::Result;
4 |
5 | use crate::{get_client, response::external::Root};
6 |
7 | const ARCHLINUX_MIRRORS: &str = "https://archlinux.org/mirrors/status/json/";
8 | const LOCAL_SOURCE: &str = include_str!("../../sample/archlinux.json");
9 |
10 | async fn response() -> Result {
11 | let client = get_client(None)?;
12 |
13 | let response = client.get(ARCHLINUX_MIRRORS).send().await;
14 |
15 | Ok(response?)
16 | }
17 |
18 | #[tokio::test]
19 | async fn arch_mirrors_ok() -> Result<()> {
20 | assert!(response().await.is_ok());
21 | assert_eq!(response().await?.status(), StatusCode::OK);
22 | Ok(())
23 | }
24 |
25 | #[tokio::test]
26 | async fn archlinux_parse_body_remote() -> Result<()> {
27 | assert!(response().await.is_ok());
28 |
29 | let root = response().await?.json::().await;
30 |
31 | assert!(root.is_ok());
32 |
33 | Ok(())
34 | }
35 |
36 | #[tokio::test]
37 | async fn archlinux_parse_body_local() -> Result<()> {
38 | assert!(serde_json::from_str::(LOCAL_SOURCE).is_ok());
39 | Ok(())
40 | }
41 |
42 | #[tokio::test]
43 | async fn check_mirrors() -> Result<()> {
44 | let mirrors = crate::get_mirrors(ARCHLINUX_MIRRORS, None);
45 | let response = crate::get_response(ARCHLINUX_MIRRORS, None);
46 | let (mirrors, response) = tokio::join!(mirrors, response);
47 | assert!(mirrors.is_ok());
48 | assert!(response.is_ok());
49 | Ok(())
50 | }
51 |
52 | #[tokio::test]
53 | async fn check_mirrors_raw() -> Result<()> {
54 | let mirrors = crate::get_mirrors_with_raw(ARCHLINUX_MIRRORS, None).await;
55 | assert!(mirrors.is_ok());
56 | Ok(())
57 | }
58 |
59 | #[tokio::test]
60 | async fn check_local_parse() -> Result<()> {
61 | let json = include_str!("../../sample/archlinux.json");
62 |
63 | let mirrors = crate::parse_local(json);
64 | assert!(mirrors.is_ok());
65 | Ok(())
66 | }
67 |
68 | #[tokio::test]
69 | #[cfg(feature = "time")]
70 | async fn check_last_sync() -> Result<()> {
71 | let client = get_client(None)?;
72 | let urls = [
73 | "https://mirror.ufs.ac.za/archlinux/",
74 | "https://cloudflaremirrors.com/archlinux/",
75 | "https://mirror.lesviallon.fr/archlinux/",
76 | ];
77 |
78 | let mut futures = Vec::with_capacity(urls.len());
79 |
80 | for i in urls.iter() {
81 | let handle = tokio::spawn({
82 | let client = client.clone();
83 | let mirror = String::from(*i);
84 | async move { crate::get_last_sync(mirror, client.clone()).await }
85 | });
86 |
87 | futures.push(handle);
88 | }
89 |
90 | let result = futures::future::try_join_all(futures).await;
91 |
92 | assert!(result.is_ok());
93 |
94 | Ok(())
95 | }
96 |
97 | #[tokio::test]
98 | #[cfg(feature = "time")]
99 | async fn rate_mirror() -> Result<()> {
100 | let client = get_client(None)?;
101 | let url = "https://mirror.ufs.ac.za/archlinux/";
102 |
103 | let res = crate::rate_mirror(url.into(), client).await;
104 | assert!(res.is_ok());
105 | Ok(())
106 | }
107 |
--------------------------------------------------------------------------------
/crates/mirro-rs/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mirro-rs"
3 | version = "0.2.3"
4 | edition = "2021"
5 | license = "MIT OR Apache-2.0"
6 | description = "An ArchLinux mirrorlist manager with a TUI"
7 | authors = ["rtkay123 "]
8 | keywords = ["http", "tui", "linux"]
9 | categories = ["command-line-interface", "command-line-utilities"]
10 | repository = "https://github.com/rtkay123/mirro-rs"
11 | homepage = "https://github.com/rtkay123/mirro-rs"
12 | documentation = "https://github.com/rtkay123/mirro-rs"
13 |
14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15 |
16 | [dependencies]
17 | ahash = "0.8.11" # https://github.com/tkaitchuck/aHash/issues/200
18 | anyhow = "1.0.82"
19 | cfg-if = { version = "1.0.0", optional = true }
20 | clap = { version = "4.5.4", features = ["derive"] }
21 | crossterm = "0.28.0"
22 | dirs = "5.0.1"
23 | itertools.workspace = true
24 | archlinux = { package = "mirrors-arch", version = "0.1.3", path = "../archlinux", features = ["time"] }
25 | notify = { version = "7.0.0", optional = true }
26 | serde = { workspace = true, features = ["derive"] }
27 | serde_json = { workspace = true, optional = true }
28 | serde_yaml = { version = "0.9.34", optional = true }
29 | tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs"] }
30 | toml = { version = "0.8.12", optional = true }
31 | tui-logger = { version = "0.13.0", features = ["crossterm", "tracing-support"], default-features = false }
32 | unicode-width = "0.1.12"
33 | ratatui = { version = "0.28.0", features = ["crossterm"], default-features = false }
34 | tracing = "0.1.40"
35 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
36 |
37 | [target.'cfg(unix)'.dependencies]
38 | tracing-journald = "0.3.0"
39 |
40 | [features]
41 | default = []
42 | json = ["dep:serde_json", "dep:notify", "dep:cfg-if"]
43 | yaml = ["dep:serde_yaml", "dep:notify", "dep:cfg-if"]
44 | toml = ["dep:toml", "dep:notify", "dep:cfg-if"]
45 |
46 | [dev-dependencies]
47 | toml = "0.8.12"
48 |
49 | [build-dependencies]
50 | clap = { version = "4.5.4", features = ["derive"] }
51 | clap_complete = "4.5.2"
52 | clap_mangen = "0.2.20"
53 | serde = { workspace = true, features = ["derive"] }
54 |
--------------------------------------------------------------------------------
/crates/mirro-rs/build.rs:
--------------------------------------------------------------------------------
1 | use clap::CommandFactory;
2 | use clap_complete::{generate_to, Shell};
3 |
4 | #[path = "src/cli/mod.rs"]
5 | mod cli;
6 |
7 | fn main() -> std::io::Result<()> {
8 | println!("cargo:rerun-if-changed=src/cli/mod.rs");
9 | if let Some(outdir) = std::env::var_os("OUT_DIR") {
10 | let outdir = std::path::PathBuf::from(outdir);
11 | let man_dir = outdir.join("man");
12 | std::fs::create_dir_all(&man_dir)?;
13 |
14 | let mut command = cli::ArgConfig::command();
15 |
16 | let man = clap_mangen::Man::new(command.clone());
17 | let mut buffer: Vec = Default::default();
18 | man.render(&mut buffer)?;
19 |
20 | std::fs::write(man_dir.join("mirro-rs.1"), buffer)?;
21 |
22 | let completions_dir = outdir.join("completions");
23 | std::fs::create_dir_all(&completions_dir)?;
24 |
25 | let crate_name = env!("CARGO_PKG_NAME");
26 |
27 | generate_to(Shell::Zsh, &mut command, crate_name, &completions_dir)?;
28 | generate_to(Shell::Bash, &mut command, crate_name, &completions_dir)?;
29 | generate_to(Shell::Fish, &mut command, crate_name, &completions_dir)?;
30 | generate_to(Shell::Elvish, &mut command, crate_name, &completions_dir)?;
31 | }
32 |
33 | Ok(())
34 | }
35 |
--------------------------------------------------------------------------------
/crates/mirro-rs/completions/_mirro-rs:
--------------------------------------------------------------------------------
1 | #compdef mirro-rs
2 |
3 | autoload -U is-at-least
4 |
5 | _mirro-rs() {
6 | typeset -A opt_args
7 | typeset -a _arguments_options
8 | local ret=1
9 |
10 | if is-at-least 5.2; then
11 | _arguments_options=(-s -S -C)
12 | else
13 | _arguments_options=(-s -C)
14 | fi
15 |
16 | local context curcontext="$curcontext" state line
17 | _arguments "${_arguments_options[@]}" \
18 | '-o+[File to write mirrors to]:OUTFILE:_files' \
19 | '--outfile=[File to write mirrors to]:OUTFILE:_files' \
20 | '-e+[Number of mirrors to export \[default: 50\]]:EXPORT: ' \
21 | '--export=[Number of mirrors to export \[default: 50\]]:EXPORT: ' \
22 | '-v+[An order to view all countries]:VIEW:(alphabetical mirror-count)' \
23 | '--view=[An order to view all countries]:VIEW:(alphabetical mirror-count)' \
24 | '-s+[Default sort for exported mirrors]:SORT:(percentage delay duration score)' \
25 | '--sort=[Default sort for exported mirrors]:SORT:(percentage delay duration score)' \
26 | '-t+[Number of hours to cache mirrorlist for]:TTL: ' \
27 | '--ttl=[Number of hours to cache mirrorlist for]:TTL: ' \
28 | '-u+[URL to check for mirrors]:URL: ' \
29 | '--url=[URL to check for mirrors]:URL: ' \
30 | '--timeout=[Connection timeout in seconds]:TIMEOUT: ' \
31 | '*-i+[Extra CDNs to check for mirrors]:INCLUDE: ' \
32 | '*--include=[Extra CDNs to check for mirrors]:INCLUDE: ' \
33 | '-a+[How old (in hours) should the mirrors be since last synchronisation]:AGE: ' \
34 | '--age=[How old (in hours) should the mirrors be since last synchronisation]:AGE: ' \
35 | '*-c+[Countries to search for mirrorlists]:COUNTRY: ' \
36 | '*-p+[Filters to use on mirrorlists]:PROTOCOLS:(https http rsync)' \
37 | '*--protocols=[Filters to use on mirrorlists]:PROTOCOLS:(https http rsync)' \
38 | '--completion-percent=[Set the minimum completion percent for the returned mirrors]:COMPLETION_PERCENT: ' \
39 | '-r[Sort mirrorlists by download speed when exporting]' \
40 | '--rate[Sort mirrorlists by download speed when exporting]' \
41 | '-d[Skip TUI session and directly export the mirrorlist]' \
42 | '--direct[Skip TUI session and directly export the mirrorlist]' \
43 | '--ipv4[Only return mirrors that support IPv4]' \
44 | '--ipv6[Only return mirrors that support IPv6]' \
45 | '--isos[Only return mirrors that host ISOs]' \
46 | '-h[Print help information]' \
47 | '--help[Print help information]' \
48 | '-V[Print version information]' \
49 | '--version[Print version information]' \
50 | && ret=0
51 | }
52 |
53 | (( $+functions[_mirro-rs_commands] )) ||
54 | _mirro-rs_commands() {
55 | local commands; commands=()
56 | _describe -t commands 'mirro-rs commands' commands "$@"
57 | }
58 |
59 | _mirro-rs "$@"
60 |
--------------------------------------------------------------------------------
/crates/mirro-rs/completions/mirro-rs.bash:
--------------------------------------------------------------------------------
1 | _mirro-rs() {
2 | local i cur prev opts cmds
3 | COMPREPLY=()
4 | cur="${COMP_WORDS[COMP_CWORD]}"
5 | prev="${COMP_WORDS[COMP_CWORD-1]}"
6 | cmd=""
7 | opts=""
8 |
9 | for i in ${COMP_WORDS[@]}
10 | do
11 | case "${cmd},${i}" in
12 | ",$1")
13 | cmd="mirro__rs"
14 | ;;
15 | *)
16 | ;;
17 | esac
18 | done
19 |
20 | case "${cmd}" in
21 | mirro__rs)
22 | opts="-o -e -v -s -t -u -r -i -d -a -c -p -h -V --outfile --export --view --sort --ttl --url --rate --timeout --include --direct --age --protocols --ipv4 --ipv6 --isos --completion-percent --help --version"
23 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
24 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
25 | return 0
26 | fi
27 | case "${prev}" in
28 | --outfile)
29 | COMPREPLY=($(compgen -f "${cur}"))
30 | return 0
31 | ;;
32 | -o)
33 | COMPREPLY=($(compgen -f "${cur}"))
34 | return 0
35 | ;;
36 | --export)
37 | COMPREPLY=($(compgen -f "${cur}"))
38 | return 0
39 | ;;
40 | -e)
41 | COMPREPLY=($(compgen -f "${cur}"))
42 | return 0
43 | ;;
44 | --view)
45 | COMPREPLY=($(compgen -W "alphabetical mirror-count" -- "${cur}"))
46 | return 0
47 | ;;
48 | -v)
49 | COMPREPLY=($(compgen -W "alphabetical mirror-count" -- "${cur}"))
50 | return 0
51 | ;;
52 | --sort)
53 | COMPREPLY=($(compgen -W "percentage delay duration score" -- "${cur}"))
54 | return 0
55 | ;;
56 | -s)
57 | COMPREPLY=($(compgen -W "percentage delay duration score" -- "${cur}"))
58 | return 0
59 | ;;
60 | --ttl)
61 | COMPREPLY=($(compgen -f "${cur}"))
62 | return 0
63 | ;;
64 | -t)
65 | COMPREPLY=($(compgen -f "${cur}"))
66 | return 0
67 | ;;
68 | --url)
69 | COMPREPLY=($(compgen -f "${cur}"))
70 | return 0
71 | ;;
72 | -u)
73 | COMPREPLY=($(compgen -f "${cur}"))
74 | return 0
75 | ;;
76 | --timeout)
77 | COMPREPLY=($(compgen -f "${cur}"))
78 | return 0
79 | ;;
80 | --include)
81 | COMPREPLY=($(compgen -f "${cur}"))
82 | return 0
83 | ;;
84 | -i)
85 | COMPREPLY=($(compgen -f "${cur}"))
86 | return 0
87 | ;;
88 | --age)
89 | COMPREPLY=($(compgen -f "${cur}"))
90 | return 0
91 | ;;
92 | -a)
93 | COMPREPLY=($(compgen -f "${cur}"))
94 | return 0
95 | ;;
96 | -c)
97 | COMPREPLY=($(compgen -f "${cur}"))
98 | return 0
99 | ;;
100 | --protocols)
101 | COMPREPLY=($(compgen -W "https http rsync" -- "${cur}"))
102 | return 0
103 | ;;
104 | -p)
105 | COMPREPLY=($(compgen -W "https http rsync" -- "${cur}"))
106 | return 0
107 | ;;
108 | --completion-percent)
109 | COMPREPLY=($(compgen -f "${cur}"))
110 | return 0
111 | ;;
112 | *)
113 | COMPREPLY=()
114 | ;;
115 | esac
116 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
117 | return 0
118 | ;;
119 | esac
120 | }
121 |
122 | complete -F _mirro-rs -o bashdefault -o default mirro-rs
123 |
--------------------------------------------------------------------------------
/crates/mirro-rs/completions/mirro-rs.elv:
--------------------------------------------------------------------------------
1 |
2 | use builtin;
3 | use str;
4 |
5 | set edit:completion:arg-completer[mirro-rs] = {|@words|
6 | fn spaces {|n|
7 | builtin:repeat $n ' ' | str:join ''
8 | }
9 | fn cand {|text desc|
10 | edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
11 | }
12 | var command = 'mirro-rs'
13 | for word $words[1..-1] {
14 | if (str:has-prefix $word '-') {
15 | break
16 | }
17 | set command = $command';'$word
18 | }
19 | var completions = [
20 | &'mirro-rs'= {
21 | cand -o 'File to write mirrors to'
22 | cand --outfile 'File to write mirrors to'
23 | cand -e 'Number of mirrors to export [default: 50]'
24 | cand --export 'Number of mirrors to export [default: 50]'
25 | cand -v 'An order to view all countries'
26 | cand --view 'An order to view all countries'
27 | cand -s 'Default sort for exported mirrors'
28 | cand --sort 'Default sort for exported mirrors'
29 | cand -t 'Number of hours to cache mirrorlist for'
30 | cand --ttl 'Number of hours to cache mirrorlist for'
31 | cand -u 'URL to check for mirrors'
32 | cand --url 'URL to check for mirrors'
33 | cand --timeout 'Connection timeout in seconds'
34 | cand -i 'Extra CDNs to check for mirrors'
35 | cand --include 'Extra CDNs to check for mirrors'
36 | cand -a 'How old (in hours) should the mirrors be since last synchronisation'
37 | cand --age 'How old (in hours) should the mirrors be since last synchronisation'
38 | cand -c 'Countries to search for mirrorlists'
39 | cand -p 'Filters to use on mirrorlists'
40 | cand --protocols 'Filters to use on mirrorlists'
41 | cand --completion-percent 'Set the minimum completion percent for the returned mirrors'
42 | cand -r 'Sort mirrorlists by download speed when exporting'
43 | cand --rate 'Sort mirrorlists by download speed when exporting'
44 | cand -d 'Skip TUI session and directly export the mirrorlist'
45 | cand --direct 'Skip TUI session and directly export the mirrorlist'
46 | cand --ipv4 'Only return mirrors that support IPv4'
47 | cand --ipv6 'Only return mirrors that support IPv6'
48 | cand --isos 'Only return mirrors that host ISOs'
49 | cand -h 'Print help information'
50 | cand --help 'Print help information'
51 | cand -V 'Print version information'
52 | cand --version 'Print version information'
53 | }
54 | ]
55 | $completions[$command]
56 | }
57 |
--------------------------------------------------------------------------------
/crates/mirro-rs/completions/mirro-rs.fish:
--------------------------------------------------------------------------------
1 | complete -c mirro-rs -s o -l outfile -d 'File to write mirrors to' -r -F
2 | complete -c mirro-rs -s e -l export -d 'Number of mirrors to export [default: 50]' -r
3 | complete -c mirro-rs -s v -l view -d 'An order to view all countries' -r -f -a "{alphabetical ,mirror-count }"
4 | complete -c mirro-rs -s s -l sort -d 'Default sort for exported mirrors' -r -f -a "{percentage ,delay ,duration ,score }"
5 | complete -c mirro-rs -s t -l ttl -d 'Number of hours to cache mirrorlist for' -r
6 | complete -c mirro-rs -s u -l url -d 'URL to check for mirrors' -r
7 | complete -c mirro-rs -l timeout -d 'Connection timeout in seconds' -r
8 | complete -c mirro-rs -s i -l include -d 'Extra CDNs to check for mirrors' -r
9 | complete -c mirro-rs -s a -l age -d 'How old (in hours) should the mirrors be since last synchronisation' -r
10 | complete -c mirro-rs -s c -d 'Countries to search for mirrorlists' -r
11 | complete -c mirro-rs -s p -l protocols -d 'Filters to use on mirrorlists' -r -f -a "{https ,http ,rsync }"
12 | complete -c mirro-rs -l completion-percent -d 'Set the minimum completion percent for the returned mirrors' -r
13 | complete -c mirro-rs -s r -l rate -d 'Sort mirrorlists by download speed when exporting'
14 | complete -c mirro-rs -s d -l direct -d 'Skip TUI session and directly export the mirrorlist'
15 | complete -c mirro-rs -l ipv4 -d 'Only return mirrors that support IPv4'
16 | complete -c mirro-rs -l ipv6 -d 'Only return mirrors that support IPv6'
17 | complete -c mirro-rs -l isos -d 'Only return mirrors that host ISOs'
18 | complete -c mirro-rs -s h -l help -d 'Print help information'
19 | complete -c mirro-rs -s V -l version -d 'Print version information'
20 |
--------------------------------------------------------------------------------
/crates/mirro-rs/man/mirro-rs.1:
--------------------------------------------------------------------------------
1 | .ie \n(.g .ds Aq \(aq
2 | .el .ds Aq '
3 | .TH mirro-rs 1 "mirro-rs 0.1.0-alpha.1"
4 | .SH NAME
5 | mirro\-rs \- An ArchLinux mirrorlist manager with a TUI
6 | .SH SYNOPSIS
7 | \fBmirro\-rs\fR [\fB\-o\fR|\fB\-\-outfile\fR] [\fB\-e\fR|\fB\-\-export\fR] [\fB\-v\fR|\fB\-\-view\fR] [\fB\-s\fR|\fB\-\-sort\fR] [\fB\-t\fR|\fB\-\-ttl\fR] [\fB\-u\fR|\fB\-\-url\fR] [\fB\-r\fR|\fB\-\-rate\fR] [\fB\-\-timeout\fR] [\fB\-i\fR|\fB\-\-include\fR] [\fB\-d\fR|\fB\-\-direct\fR] [\fB\-a\fR|\fB\-\-age\fR] [\fB\-c \fR] [\fB\-p\fR|\fB\-\-protocols\fR] [\fB\-\-ipv4\fR] [\fB\-\-ipv6\fR] [\fB\-\-isos\fR] [\fB\-\-completion\-percent\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
8 | .SH DESCRIPTION
9 | An ArchLinux mirrorlist manager with a TUI
10 | .SH OPTIONS
11 | .TP
12 | \fB\-o\fR, \fB\-\-outfile\fR=\fIOUTFILE\fR
13 | File to write mirrors to
14 | .TP
15 | \fB\-e\fR, \fB\-\-export\fR=\fIEXPORT\fR
16 | Number of mirrors to export [default: 50]
17 | .TP
18 | \fB\-v\fR, \fB\-\-view\fR=\fIVIEW\fR
19 | An order to view all countries
20 | .br
21 |
22 | .br
23 | [\fIpossible values: \fRalphabetical, mirror\-count]
24 | .TP
25 | \fB\-s\fR, \fB\-\-sort\fR=\fISORT\fR
26 | Default sort for exported mirrors
27 | .br
28 |
29 | .br
30 | [\fIpossible values: \fRpercentage, delay, duration, score]
31 | .TP
32 | \fB\-t\fR, \fB\-\-ttl\fR=\fITTL\fR
33 | Number of hours to cache mirrorlist for
34 | .TP
35 | \fB\-u\fR, \fB\-\-url\fR=\fIURL\fR
36 | URL to check for mirrors
37 | .TP
38 | \fB\-r\fR, \fB\-\-rate\fR=\fIRATE\fR
39 | Sort mirrorlists by download speed when exporting
40 | .TP
41 | \fB\-\-timeout\fR=\fITIMEOUT\fR
42 | Connection timeout in seconds
43 | .TP
44 | \fB\-i\fR, \fB\-\-include\fR=\fIINCLUDE\fR
45 | Extra CDNs to check for mirrors
46 | .TP
47 | \fB\-d\fR, \fB\-\-direct\fR=\fIDIRECT\fR
48 | Skip TUI session and directly export the mirrorlist
49 | .TP
50 | \fB\-a\fR, \fB\-\-age\fR=\fIAGE\fR
51 | How old (in hours) should the mirrors be since last synchronisation
52 | .TP
53 | \fB\-c\fR=\fICOUNTRY\fR
54 | Countries to search for mirrorlists
55 | .TP
56 | \fB\-p\fR, \fB\-\-protocols\fR=\fIPROTOCOLS\fR
57 | Filters to use on mirrorlists
58 | .br
59 |
60 | .br
61 | [\fIpossible values: \fRhttps, http, rsync]
62 | .TP
63 | \fB\-\-ipv4\fR=\fIIPV4\fR
64 | Only return mirrors that support IPv4
65 | .TP
66 | \fB\-\-ipv6\fR=\fIIPV6\fR
67 | Only return mirrors that support IPv6
68 | .TP
69 | \fB\-\-isos\fR=\fIISOS\fR
70 | Only return mirrors that host ISOs
71 | .TP
72 | \fB\-\-completion\-percent\fR=\fICOMPLETION_PERCENT\fR
73 | Set the minimum completion percent for the returned mirrors
74 | .TP
75 | \fB\-h\fR, \fB\-\-help\fR
76 | Print help information
77 | .TP
78 | \fB\-V\fR, \fB\-\-version\fR
79 | Print version information
80 | .SH VERSION
81 | v0.1.0\-alpha.1
82 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/cli/mod.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use clap::{Parser, ValueEnum};
4 | use serde::Deserialize;
5 |
6 | pub const DEFAULT_MIRROR_COUNT: u16 = 50;
7 | pub const DEFAULT_CACHE_TTL: u16 = 24;
8 | pub const ARCH_URL: &str = "https://archlinux.org/mirrors/status/json/";
9 |
10 | #[cfg_attr(test, derive(Default))]
11 | #[derive(Parser, Debug, Deserialize)]
12 | #[command(author, version, about, long_about = None)]
13 | pub struct ArgConfig {
14 | #[command(flatten)]
15 | pub general: Args,
16 | #[command(flatten)]
17 | pub filters: Filters,
18 | }
19 |
20 | #[cfg_attr(test, derive(Default))]
21 | #[derive(clap::Args, Debug, Deserialize)]
22 | #[command(author, version, about, long_about = None)]
23 | pub struct Args {
24 | /// File to write mirrors to
25 | #[arg(short, long)]
26 | pub outfile: Option,
27 |
28 | /// Number of mirrors to export [default: 50]
29 | #[arg(short, long)]
30 | #[serde(default = "default_export")]
31 | pub export: Option,
32 |
33 | /// An order to view all countries
34 | #[arg(short, long, value_enum)]
35 | #[serde(default = "view")]
36 | pub view: Option,
37 |
38 | /// Default sort for exported mirrors
39 | #[arg(short, long, value_enum)]
40 | #[serde(default = "sort")]
41 | pub sort: Option,
42 |
43 | /// Number of hours to cache mirrorlist for
44 | #[arg(short, long)]
45 | #[serde(rename = "cache-ttl")]
46 | #[serde(default = "default_ttl")]
47 | pub ttl: Option,
48 |
49 | /// URL to check for mirrors
50 | #[arg(short, long)]
51 | #[serde(default = "url")]
52 | pub url: Option,
53 |
54 | /// Specify alternate configuration file
55 | #[arg(long)]
56 | #[serde(skip)]
57 | #[cfg(any(feature = "toml", feature = "yaml", feature = "json"))]
58 | pub config: Option,
59 |
60 | /// Sort mirrorlists by download speed when exporting
61 | #[arg(short, long)]
62 | #[serde(default, rename = "rate-speed")]
63 | pub rate: bool,
64 |
65 | /// Connection timeout in seconds
66 | #[arg(long = "timeout")]
67 | pub timeout: Option,
68 |
69 | /// Extra CDNs to check for mirrors
70 | #[arg(short, long)]
71 | pub include: Option>,
72 |
73 | /// Skip TUI session and directly export the mirrorlist
74 | #[arg(short, long)]
75 | #[serde(default)]
76 | pub direct: bool,
77 | }
78 |
79 | #[cfg_attr(test, derive(Default))]
80 | #[derive(clap::Args, Debug, Clone, Eq, PartialEq, Deserialize)]
81 | pub struct Filters {
82 | /// How old (in hours) should the mirrors be since last synchronisation
83 | #[arg(long, short)]
84 | pub age: Option,
85 |
86 | /// Countries to search for mirrorlists
87 | #[arg(short)]
88 | #[serde(rename = "countries")]
89 | #[serde(default)]
90 | pub country: Option>,
91 |
92 | /// Filters to use on mirrorlists
93 | #[arg(short, long, value_enum)]
94 | #[serde(default = "filters")]
95 | pub protocols: Option>,
96 |
97 | ///Only return mirrors that support IPv4.
98 | #[arg(long)]
99 | #[serde(default = "enable")]
100 | pub ipv4: bool,
101 | ///Only return mirrors that support IPv6.
102 | #[arg(long)]
103 | #[serde(default = "enable")]
104 | pub ipv6: bool,
105 | /// Only return mirrors that host ISOs.
106 | #[arg(long)]
107 | #[serde(default = "enable")]
108 | pub isos: bool,
109 |
110 | /// Set the minimum completion percent for the returned mirrors.
111 | #[arg(long)]
112 | #[serde(default = "completion", rename = "completion-percent")]
113 | pub completion_percent: Option,
114 | }
115 |
116 | #[derive(Default, Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, ValueEnum, Deserialize)]
117 | #[serde(rename_all = "lowercase")]
118 | pub enum SelectionSort {
119 | Percentage,
120 | Delay,
121 | Duration,
122 | #[default]
123 | Score,
124 | }
125 |
126 | fn enable() -> bool {
127 | true
128 | }
129 |
130 | fn completion() -> Option {
131 | Some(100)
132 | }
133 |
134 | fn url() -> Option {
135 | Some(ARCH_URL.to_string())
136 | }
137 |
138 | fn default_ttl() -> Option {
139 | Some(DEFAULT_CACHE_TTL)
140 | }
141 |
142 | fn default_export() -> Option {
143 | Some(DEFAULT_MIRROR_COUNT)
144 | }
145 |
146 | fn sort() -> Option {
147 | Some(SelectionSort::Score)
148 | }
149 |
150 | fn view() -> Option {
151 | Some(ViewSort::Alphabetical)
152 | }
153 |
154 | fn filters() -> Option> {
155 | Some(vec![Protocol::Http, Protocol::Https])
156 | }
157 |
158 | #[cfg_attr(test, derive(Default))]
159 | #[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, ValueEnum, Deserialize)]
160 | #[serde(rename_all = "lowercase")]
161 | pub enum Protocol {
162 | #[cfg_attr(test, default)]
163 | Https,
164 | Http,
165 | Rsync,
166 | Ftp,
167 | #[value(skip)]
168 | InSync,
169 | #[value(skip)]
170 | Ipv4,
171 | #[value(skip)]
172 | Ipv6,
173 | #[value(skip)]
174 | Isos,
175 | }
176 |
177 | #[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, ValueEnum, Deserialize, Default)]
178 | #[serde(rename_all = "lowercase")]
179 | pub enum ViewSort {
180 | #[default]
181 | Alphabetical,
182 | MirrorCount,
183 | }
184 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/config/file.rs:
--------------------------------------------------------------------------------
1 | use std::{io::ErrorKind, path::PathBuf};
2 |
3 | use std::path::Path;
4 |
5 | use itertools::Itertools;
6 | use tracing::error;
7 |
8 | use crate::cli::ArgConfig;
9 |
10 | cfg_if::cfg_if! {
11 | if #[cfg(feature = "toml")] {
12 | fn default_args()->ArgConfig {
13 | let config_str = include_str!("../../../../examples/mirro-rs.toml");
14 | toml::from_str(config_str).unwrap()
15 | }
16 | } else if #[cfg(feature = "yaml")] {
17 | fn default_args()->ArgConfig {
18 | let config_str = include_str!("../../../../examples/mirro-rs.yaml");
19 | serde_yaml::from_str(config_str).unwrap()
20 | }
21 | } else {
22 | fn default_args()->ArgConfig {
23 | let config_str = include_str!("../../../../examples/mirro-rs.json");
24 | serde_json::from_str(config_str).unwrap()
25 | }
26 | }
27 | }
28 |
29 | pub fn read_config_file(file: Option>) -> (ArgConfig, Option) {
30 | let config_file = if let Some(ref file) = file {
31 | let buf = file.as_ref().to_path_buf();
32 | Some(check_file(&buf, None))
33 | } else {
34 | dirs::config_dir().map(|dir| get_config(dir, &extensions()))
35 | };
36 | match config_file {
37 | Some(Some(opts)) => opts,
38 | _ => (default_args(), None),
39 | }
40 | }
41 |
42 | fn check_file(file: &PathBuf, backup: Option<&PathBuf>) -> Option<(ArgConfig, Option)> {
43 | let err = |e| {
44 | error!("{e}");
45 | };
46 |
47 | let call_backup = |backup: Option<&PathBuf>| {
48 | if let Some(backup) = backup {
49 | check_file(backup, None)
50 | } else {
51 | None
52 | }
53 | };
54 |
55 | let f = std::fs::read_to_string(file);
56 |
57 | let err_type = || -> Result {
58 | let ext = String::from_iter(extensions());
59 | Err(format!("unsupported file extension: file must be: {ext}"))
60 | };
61 |
62 | match f {
63 | #[allow(unused_variables)]
64 | Ok(contents) => {
65 | let result = if let Some(ext) = file.extension() {
66 | match ext.to_string_lossy().to_string().as_str() {
67 | #[cfg(feature = "toml")]
68 | "toml" => toml::from_str::(&contents).map_err(|e| e.to_string()),
69 | #[cfg(feature = "json")]
70 | "json" => {
71 | serde_json::from_str::(&contents).map_err(|e| e.to_string())
72 | }
73 | #[cfg(feature = "yaml")]
74 | "yaml" | "yml" => {
75 | serde_yaml::from_str::(&contents).map_err(|e| e.to_string())
76 | }
77 | _ => err_type(),
78 | }
79 | } else {
80 | err_type()
81 | };
82 |
83 | match result {
84 | Ok(e) => Some((e, Some(file.to_owned()))),
85 | Err(e) => {
86 | err(format!("config: {} -> {}", file.display(), e));
87 | call_backup(backup)
88 | }
89 | }
90 | }
91 | Err(e) => {
92 | if e.kind() != ErrorKind::NotFound {
93 | err(format!("config: {} -> {}", file.display(), e));
94 | }
95 | call_backup(backup)
96 | }
97 | }
98 | }
99 |
100 | fn extensions() -> Vec {
101 | let valid_extensions = vec![
102 | #[cfg(feature = "toml")]
103 | "toml",
104 | #[cfg(feature = "json")]
105 | "json",
106 | #[cfg(feature = "yaml")]
107 | "yaml",
108 | #[cfg(feature = "yaml")]
109 | "yml",
110 | ];
111 |
112 | valid_extensions.into_iter().map(String::from).collect_vec()
113 | }
114 |
115 | fn get_config(mut dir: PathBuf, extension: &[String]) -> Option<(ArgConfig, Option)> {
116 | let crate_name = env!("CARGO_PKG_NAME");
117 | let location = PathBuf::from(crate_name);
118 | let mut file = PathBuf::from(crate_name);
119 | let mut result: Option<(ArgConfig, Option)> = None;
120 | for i in extension.iter() {
121 | let mut inner_location = location.clone();
122 | file.set_extension(i);
123 | inner_location.push(file.clone());
124 |
125 | let mut alt = dir.clone();
126 | dir.push(inner_location);
127 | alt.push(file.clone());
128 | let interim = check_file(&dir, Some(&alt));
129 | if interim.is_some() {
130 | result = interim;
131 | break;
132 | }
133 | }
134 | result
135 | }
136 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/config/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
2 | mod file;
3 |
4 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
5 | mod watch;
6 |
7 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
8 | pub use watch::watch_config;
9 |
10 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
11 | pub use file::read_config_file;
12 |
13 | use std::path::PathBuf;
14 |
15 | use crate::{
16 | cli::{self, ArgConfig, Protocol, SelectionSort, ViewSort},
17 | tui::view::sort::ExportSort,
18 | };
19 |
20 | #[cfg_attr(test, derive(Default))]
21 | #[derive(Debug)]
22 | pub struct Configuration {
23 | pub outfile: PathBuf,
24 | pub export: u16,
25 | pub filters: Vec,
26 | pub view: ViewSort,
27 | pub sort: ExportSort,
28 | pub country: Vec,
29 | pub ttl: u16,
30 | pub url: String,
31 | pub completion_percent: u8,
32 | pub age: u16,
33 | pub rate: bool,
34 | pub connection_timeout: Option,
35 | pub include: Option>,
36 | pub direct: bool,
37 | }
38 |
39 | impl Configuration {
40 | #[allow(clippy::too_many_arguments)]
41 | pub fn new(
42 | outfile: PathBuf,
43 | export: u16,
44 | mut filters: Vec,
45 | view: ViewSort,
46 | sort: SelectionSort,
47 | country: Vec,
48 | ttl: u16,
49 | url: String,
50 | ipv4: bool,
51 | isos: bool,
52 | ipv6: bool,
53 | completion_percent: u8,
54 | age: u16,
55 | rate: bool,
56 | connection_timeout: Option,
57 | include: Option>,
58 | direct: bool,
59 | ) -> Self {
60 | if ipv4 {
61 | filters.push(Protocol::Ipv4)
62 | }
63 | if ipv6 {
64 | filters.push(Protocol::Ipv6)
65 | }
66 | if isos {
67 | filters.push(Protocol::Isos)
68 | }
69 | Self {
70 | outfile,
71 | export,
72 | filters,
73 | view,
74 | sort: match sort {
75 | SelectionSort::Percentage => ExportSort::Completion,
76 | SelectionSort::Delay => ExportSort::MirroringDelay,
77 | SelectionSort::Duration => ExportSort::Duration,
78 | SelectionSort::Score => ExportSort::Score,
79 | },
80 | country,
81 | ttl,
82 | url,
83 | completion_percent,
84 | age,
85 | rate,
86 | connection_timeout,
87 | include,
88 | direct,
89 | }
90 | }
91 | }
92 |
93 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
94 | fn get_bools(args: &cli::Filters, config: &cli::Filters) -> (bool, bool, bool) {
95 | let ipv4 = if !args.ipv4 && config.ipv4 {
96 | true
97 | } else {
98 | args.ipv4
99 | };
100 |
101 | let ipv6 = if !args.ipv6 && config.ipv6 {
102 | true
103 | } else {
104 | args.ipv6
105 | };
106 |
107 | let isos = if !args.isos && config.isos {
108 | true
109 | } else {
110 | args.isos
111 | };
112 |
113 | (ipv4, ipv6, isos)
114 | }
115 |
116 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
117 | impl From<(ArgConfig, ArgConfig)> for Configuration {
118 | fn from((mut args, mut config): (ArgConfig, ArgConfig)) -> Self {
119 | let (ipv4, isos, ipv6) = get_bools(&args.filters, &config.filters);
120 | let outfile = args
121 | .general
122 | .outfile
123 | .unwrap_or_else(|| config.general.outfile.unwrap());
124 | let export = args
125 | .general
126 | .export
127 | .unwrap_or_else(|| config.general.export.unwrap());
128 | let filters = args
129 | .filters
130 | .protocols
131 | .unwrap_or_else(|| config.filters.protocols.unwrap());
132 | let view = args
133 | .general
134 | .view
135 | .unwrap_or_else(|| config.general.view.unwrap());
136 | let sort = args
137 | .general
138 | .sort
139 | .unwrap_or_else(|| config.general.sort.unwrap());
140 | let countries = args
141 | .filters
142 | .country
143 | .unwrap_or_else(|| config.filters.country.unwrap());
144 | let ttl = args
145 | .general
146 | .ttl
147 | .unwrap_or_else(|| config.general.ttl.unwrap());
148 | let url = args
149 | .general
150 | .url
151 | .unwrap_or_else(|| config.general.url.unwrap());
152 |
153 | let completion = args
154 | .filters
155 | .completion_percent
156 | .unwrap_or_else(|| config.filters.completion_percent.unwrap());
157 |
158 | let age = args
159 | .filters
160 | .age
161 | .unwrap_or_else(|| config.filters.age.unwrap_or_default());
162 |
163 | let rate = if !args.general.rate && config.general.rate {
164 | true
165 | } else {
166 | args.general.rate
167 | };
168 |
169 | let timoeut = if args.general.timeout.is_none() && config.general.timeout.is_some() {
170 | config.general.timeout
171 | } else {
172 | args.general.timeout
173 | };
174 |
175 | let include = if args.general.include.is_none() && config.general.include.is_some() {
176 | std::mem::take(&mut config.general.include)
177 | } else {
178 | std::mem::take(&mut args.general.include)
179 | };
180 | let direct = if !args.general.direct && config.general.direct {
181 | true
182 | } else {
183 | args.general.direct
184 | };
185 |
186 | Self::new(
187 | outfile, export, filters, view, sort, countries, ttl, url, ipv4, isos, ipv6,
188 | completion, age, rate, timoeut, include, direct,
189 | )
190 | }
191 | }
192 |
193 | #[cfg(any(test, not(any(feature = "json", feature = "toml", feature = "yaml"))))]
194 | impl From for Configuration {
195 | fn from(args: ArgConfig) -> Self {
196 | let outfile = args
197 | .general
198 | .outfile
199 | .or_else(|| crate::exit("outfile"))
200 | .unwrap();
201 | let export = args.general.export.unwrap_or(cli::DEFAULT_MIRROR_COUNT);
202 | let filters = args
203 | .filters
204 | .protocols
205 | .unwrap_or_else(|| vec![Protocol::Http, Protocol::Https]);
206 | let view = args.general.view.unwrap_or_default();
207 | let sort = args.general.sort.unwrap_or_default();
208 | let countries = args.filters.country.unwrap_or_default();
209 | let ttl = args.general.ttl.unwrap_or(cli::DEFAULT_CACHE_TTL);
210 | let url = args
211 | .general
212 | .url
213 | .unwrap_or_else(|| cli::ARCH_URL.to_string());
214 |
215 | let completion = args.filters.completion_percent.unwrap_or(100);
216 |
217 | let age = args.filters.age.unwrap_or(0);
218 | let rate = args.general.rate;
219 | let timeout = args.general.timeout;
220 | let include = args.general.include;
221 |
222 | Self::new(
223 | outfile,
224 | export,
225 | filters,
226 | view,
227 | sort,
228 | countries,
229 | ttl,
230 | url,
231 | args.filters.ipv4,
232 | args.filters.isos,
233 | args.filters.ipv6,
234 | completion,
235 | age,
236 | rate,
237 | timeout,
238 | include,
239 | args.general.direct,
240 | )
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/config/watch.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::Debug,
3 | path::Path,
4 | path::PathBuf,
5 | sync::{mpsc, Arc, Mutex},
6 | };
7 |
8 | use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
9 | use tracing::error;
10 |
11 | use crate::config::read_config_file;
12 |
13 | use super::Configuration;
14 |
15 | pub fn watch_config(path: Option, configuration: Arc>) {
16 | if let Some(dir) = dirs::config_dir() {
17 | if let Some(path) = path {
18 | tokio::task::spawn_blocking(move || {
19 | if let Err(e) = async_watch(path, dir, configuration) {
20 | error!("error: {:?}", e)
21 | }
22 | });
23 | }
24 | }
25 | }
26 |
27 | fn async_watcher() -> notify::Result<(RecommendedWatcher, mpsc::Receiver>)> {
28 | let (tx, rx) = mpsc::channel();
29 |
30 | // Automatically select the best implementation for your platform.
31 | // You can also access each implementation directly e.g. INotifyWatcher.
32 | let watcher = RecommendedWatcher::new(
33 | move |res| {
34 | let _ = tx.send(res);
35 | },
36 | Config::default(),
37 | )?;
38 |
39 | Ok((watcher, rx))
40 | }
41 |
42 | fn async_watch(
43 | path: impl AsRef + Debug,
44 | dir: impl AsRef + Debug,
45 | config: Arc>,
46 | ) -> notify::Result<()> {
47 | let (mut watcher, rx) = async_watcher()?;
48 |
49 | // Add a path to be watched. All files and directories at that path and
50 | // below will be monitored for changes.
51 |
52 | watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?;
53 |
54 | while let Ok(res) = rx.recv() {
55 | match res {
56 | Ok(event) => {
57 | if event
58 | .paths
59 | .iter()
60 | .any(|f| f.file_name() == path.as_ref().file_name())
61 | {
62 | let (config_file, _) = read_config_file(Some(path.as_ref().to_path_buf()));
63 | let parsed_config = Configuration::new(
64 | config_file.general.outfile.unwrap(),
65 | config_file.general.export.unwrap(),
66 | config_file.filters.protocols.unwrap(),
67 | config_file.general.view.unwrap(),
68 | config_file.general.sort.unwrap(),
69 | config_file.filters.country.unwrap(),
70 | config_file.general.ttl.unwrap(),
71 | config_file.general.url.unwrap(),
72 | config_file.filters.ipv4,
73 | config_file.filters.isos,
74 | config_file.filters.ipv6,
75 | config_file.filters.completion_percent.unwrap(),
76 | config_file.filters.age.unwrap_or_default(),
77 | config_file.general.rate,
78 | config_file.general.timeout,
79 | config_file.general.include,
80 | config_file.general.direct,
81 | );
82 |
83 | let mut new_config = config.lock().unwrap();
84 | *new_config = parsed_config;
85 | }
86 | }
87 | Err(e) => error!("watch error: {:?}", e),
88 | }
89 | }
90 | Ok(())
91 | }
92 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/dbg/mod.rs:
--------------------------------------------------------------------------------
1 | use std::io::Error;
2 |
3 | use tracing::info;
4 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
5 |
6 | pub fn log(skip_tui: bool) {
7 | let registry = tracing_subscriber::registry().with(
8 | tracing_subscriber::EnvFilter::try_from_default_env()
9 | .unwrap_or_else(|_| "mirro_rs=debug".into()),
10 | );
11 |
12 | let err_fn = |e: Error| {
13 | #[cfg(unix)]
14 | tracing::error!("couldn't connect to journald: {}", e);
15 | };
16 |
17 | #[cfg(unix)]
18 | match (tracing_journald::layer(), skip_tui) {
19 | (Ok(layer), true) => {
20 | registry
21 | .with(layer)
22 | .with(tracing_subscriber::fmt::layer())
23 | .init();
24 | }
25 | // journald is typically available on Linux systems, but nowhere else. Portable software
26 | // should handle its absence gracefully.
27 | (Err(e), true) => {
28 | registry.with(tracing_subscriber::fmt::layer()).init();
29 | err_fn(e);
30 | }
31 | (Ok(layer), false) => {
32 | registry
33 | .with(layer)
34 | .with(tui_logger::tracing_subscriber_layer())
35 | .init();
36 | }
37 | (Err(e), false) => {
38 | registry.with(tui_logger::tracing_subscriber_layer()).init();
39 | err_fn(e);
40 | }
41 | }
42 |
43 | #[cfg(not(unix))]
44 | if skip_tui {
45 | registry.with(tracing_subscriber::fmt::layer()).init();
46 | } else {
47 | registry.with(tui_logger::tracing_subscriber_layer()).init();
48 | }
49 |
50 | let pkg_ver = env!("CARGO_PKG_VERSION");
51 | info!(version = pkg_ver, "mirro-rs has started");
52 | }
53 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/direct/mod.rs:
--------------------------------------------------------------------------------
1 | use std::sync::{Arc, Mutex};
2 |
3 | use anyhow::{bail, Context, Result};
4 | use archlinux::{
5 | chrono::{DateTime, Local},
6 | get_client, ArchLinux, Mirror,
7 | };
8 | use itertools::Itertools;
9 | use tracing::error;
10 |
11 | use crate::{
12 | cli::Protocol,
13 | config::Configuration,
14 | tui::io::{self, handler::IoAsyncHandler},
15 | };
16 |
17 | pub async fn begin(configuration: Configuration) -> Result<()> {
18 | let included = configuration.include.clone();
19 | let connection_timeout = configuration.connection_timeout;
20 | let rate = configuration.rate;
21 | let outfile = configuration.outfile.clone();
22 | let export_count = configuration.export;
23 |
24 | let config = Arc::new(Mutex::new(configuration));
25 | let (is_fresh, cache_file) = io::handler::is_fresh(Arc::clone(&config)).await;
26 | let mirrorlist = if is_fresh {
27 | match tokio::fs::read_to_string(cache_file.as_ref().unwrap()).await {
28 | Ok(contents) => {
29 | let result = archlinux::parse_local(&contents);
30 | match result {
31 | Ok(mirrors) => mirrors,
32 | Err(e) => {
33 | error!("{e}");
34 | get_new_mirrors(Arc::clone(&config), cache_file.as_ref()).await?
35 | }
36 | }
37 | }
38 | Err(e) => {
39 | error!("{e}");
40 | get_new_mirrors(Arc::clone(&config), cache_file.as_ref()).await?
41 | }
42 | }
43 | } else {
44 | get_new_mirrors(Arc::clone(&config), cache_file.as_ref()).await?
45 | };
46 |
47 | let mut results = mirrorlist
48 | .countries
49 | .iter()
50 | .filter_map(|f| {
51 | let results = f
52 | .mirrors
53 | .iter()
54 | .filter(|f| filter_result(f, Arc::clone(&config)))
55 | .filter(|_| {
56 | let conf = config.lock().unwrap();
57 |
58 | if conf.country.is_empty() {
59 | true
60 | } else {
61 | conf.country.iter().any(|b| b.eq_ignore_ascii_case(&f.name))
62 | }
63 | })
64 | .collect_vec();
65 | if results.is_empty() {
66 | None
67 | } else {
68 | Some(results)
69 | }
70 | })
71 | .flatten()
72 | .map(|f| f.url.clone())
73 | .collect_vec();
74 |
75 | if let Some(mut included) = included {
76 | results.append(&mut included);
77 | }
78 |
79 | let client = get_client(connection_timeout)?;
80 |
81 | if rate {
82 | if let Err(e) = IoAsyncHandler::rate_mirrors(
83 | results,
84 | None,
85 | None,
86 | outfile,
87 | export_count.into(),
88 | None,
89 | client,
90 | )
91 | .await
92 | .await
93 | {
94 | error!("{e}");
95 | }
96 | } else {
97 | IoAsyncHandler::write_to_file(outfile, &results, export_count as usize, None, None).await;
98 | }
99 |
100 | Ok(())
101 | }
102 |
103 | async fn get_new_mirrors(
104 | config: Arc>,
105 | cache_file: Option<&std::path::PathBuf>,
106 | ) -> Result {
107 | let (url, timeout) = {
108 | let config = config.lock().unwrap();
109 | (config.url.clone(), config.connection_timeout)
110 | };
111 |
112 | match archlinux::get_mirrors_with_raw(&url, timeout).await {
113 | Ok((resp, str_value)) => {
114 | if let Some(cache) = cache_file {
115 | if let Err(e) = tokio::fs::write(cache, str_value).await {
116 | error!("{e}");
117 | }
118 | }
119 | Ok(resp)
120 | }
121 | Err(e) => {
122 | error!("{e}");
123 | if let Some(f) = cache_file {
124 | tokio::fs::read_to_string(f)
125 | .await
126 | .ok()
127 | .and_then(|contents| archlinux::parse_local(&contents).ok())
128 | .context("could not read cache file")
129 | } else {
130 | bail!("No cache file was configured")
131 | }
132 | }
133 | }
134 | }
135 |
136 | pub fn filter_result(f: &Mirror, configuration: Arc>) -> bool {
137 | let mut config = configuration.lock().unwrap();
138 |
139 | let res = |config: &Configuration, f: &Mirror| {
140 | let mut completion_ok = config.completion_percent as f32 <= f.completion_pct * 100.0;
141 | let v4_on = config.filters.contains(&Protocol::Ipv4);
142 | let isos_on = config.filters.contains(&Protocol::Isos);
143 | let v6_on = config.filters.contains(&Protocol::Ipv6);
144 | if v4_on {
145 | completion_ok = completion_ok && f.ipv4;
146 | }
147 |
148 | if isos_on {
149 | completion_ok = completion_ok && f.isos;
150 | }
151 |
152 | if v6_on {
153 | completion_ok = completion_ok && f.ipv6;
154 | }
155 | completion_ok
156 | };
157 |
158 | if config.age != 0 {
159 | if let Some(mirror_sync) = f.last_sync {
160 | let now = Local::now();
161 | let mirror_sync: DateTime = DateTime::from(mirror_sync);
162 | let duration = now - mirror_sync;
163 | if !config.filters.contains(&Protocol::InSync) {
164 | config.filters.push(Protocol::InSync);
165 | }
166 | duration.num_hours() <= config.age.into()
167 | && config.filters.contains(&Protocol::from(f.protocol))
168 | && res(&config, f)
169 | } else {
170 | false
171 | }
172 | } else {
173 | config.filters.contains(&Protocol::from(f.protocol)) && res(&config, f)
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/main.rs:
--------------------------------------------------------------------------------
1 | mod cli;
2 | mod config;
3 | mod dbg;
4 | mod direct;
5 | #[cfg(test)]
6 | mod test;
7 | mod tui;
8 |
9 | use std::sync::{Arc, Mutex};
10 |
11 | use tracing::error;
12 |
13 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
14 | use self::config::watch_config;
15 |
16 | #[tokio::main]
17 | async fn main() {
18 | let args = ::parse();
19 |
20 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
21 | let (config, file) = config::read_config_file(args.general.config.as_ref());
22 |
23 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
24 | if !check_outfile(&args.general) && !check_outfile(&config.general) {
25 | exit("outfile");
26 | }
27 |
28 | #[cfg(not(any(feature = "json", feature = "toml", feature = "yaml")))]
29 | if !check_outfile(&args.general) {
30 | exit("outfile");
31 | }
32 |
33 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
34 | let config = config::Configuration::from((args, config));
35 |
36 | #[cfg(not(any(feature = "json", feature = "toml", feature = "yaml")))]
37 | let config = config::Configuration::from(args);
38 |
39 | dbg::log(config.direct);
40 |
41 | if config.direct {
42 | if let Err(e) = direct::begin(config).await {
43 | error!("{e}")
44 | }
45 | } else {
46 | let config = Arc::new(Mutex::new(config));
47 |
48 | #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
49 | watch_config(file, Arc::clone(&config));
50 |
51 | let _ = tui::start(config).await;
52 | }
53 | std::process::exit(0);
54 | }
55 |
56 | pub fn exit(value: &str) -> ! {
57 | let cmd = clap::Command::new("mirro-rs");
58 | let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(&cmd);
59 | err.insert(
60 | clap::error::ContextKind::InvalidArg,
61 | clap::error::ContextValue::String(format!("--{value}")),
62 | );
63 |
64 | err.insert(
65 | clap::error::ContextKind::InvalidValue,
66 | clap::error::ContextValue::String(String::default()),
67 | );
68 | err.exit();
69 | }
70 |
71 | fn check_outfile(config: &cli::Args) -> bool {
72 | if let Some(ref outfile) = config.outfile {
73 | if outfile.to_string_lossy().ends_with('/') || outfile.to_string_lossy().is_empty() {
74 | exit("outfile");
75 | }
76 | true
77 | } else {
78 | false
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/test/mod.rs:
--------------------------------------------------------------------------------
1 | use crate::{cli::ArgConfig, config::Configuration, direct::begin};
2 |
3 | #[tokio::test]
4 | async fn sample_bin() {
5 | let config_str = include_str!("../../../../examples/mirro-rs.toml");
6 | let configuration: ArgConfig = toml::from_str(config_str).unwrap();
7 | let config = Configuration::from(configuration);
8 | let result = begin(config).await;
9 | dbg!(&result);
10 | assert!(result.is_ok());
11 | }
12 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/actions.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, fmt::Display, slice::Iter};
2 |
3 | use super::inputs::key::Key;
4 |
5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6 | pub enum Action {
7 | ClosePopUp,
8 | Quit,
9 | ShowInput,
10 | NavigateUp,
11 | NavigateDown,
12 | FilterHttps,
13 | FilterHttp,
14 | FilterFtp,
15 | FilterRsync,
16 | FilterSyncing,
17 | FilterIpv4,
18 | FilterIpv6,
19 | FilterIsos,
20 | ViewSortAlphabetically,
21 | ViewSortMirrorCount,
22 | ToggleSelect,
23 | SelectionSortCompletionPct,
24 | SelectionSortDelay,
25 | SelectionSortDuration,
26 | SelectionSortScore,
27 | Export,
28 | }
29 |
30 | impl Action {
31 | pub fn iterator() -> Iter<'static, Action> {
32 | static ACTIONS: [Action; 21] = [
33 | Action::Quit,
34 | Action::ClosePopUp,
35 | Action::ShowInput,
36 | Action::NavigateUp,
37 | Action::NavigateDown,
38 | Action::FilterHttp,
39 | Action::FilterHttps,
40 | Action::FilterRsync,
41 | Action::FilterFtp,
42 | Action::FilterIpv4,
43 | Action::FilterIpv6,
44 | Action::FilterIsos,
45 | Action::FilterSyncing,
46 | Action::ViewSortMirrorCount,
47 | Action::ViewSortAlphabetically,
48 | Action::ToggleSelect,
49 | Action::SelectionSortCompletionPct,
50 | Action::SelectionSortDelay,
51 | Action::SelectionSortDuration,
52 | Action::SelectionSortScore,
53 | Action::Export,
54 | ];
55 | ACTIONS.iter()
56 | }
57 |
58 | pub fn keys(&self) -> &[Key] {
59 | match self {
60 | Action::Quit => &[Key::Ctrl('c'), Key::Char('q')],
61 | Action::ClosePopUp => &[Key::Ctrl('p')],
62 | Action::ShowInput => &[Key::Ctrl('i'), Key::Char('/')],
63 | Action::NavigateUp => &[Key::Char('k'), Key::Up],
64 | Action::NavigateDown => &[Key::Char('j'), Key::Down],
65 | Action::FilterHttps => &[Key::Ctrl('s')],
66 | Action::FilterHttp => &[Key::Ctrl('h')],
67 | Action::FilterRsync => &[Key::Ctrl('r')],
68 | Action::FilterFtp => &[Key::Ctrl('f')],
69 | Action::FilterSyncing => &[Key::Ctrl('o')],
70 | Action::ViewSortAlphabetically => &[Key::Char('1')],
71 | Action::ViewSortMirrorCount => &[Key::Char('2')],
72 | Action::ToggleSelect => &[Key::Char(' ')],
73 | Action::SelectionSortCompletionPct => &[Key::Char('5')],
74 | Action::SelectionSortDelay => &[Key::Char('6')],
75 | Action::SelectionSortDuration => &[Key::Char('7')],
76 | Action::SelectionSortScore => &[Key::Char('8')],
77 | Action::Export => &[Key::Ctrl('e')],
78 | Action::FilterIpv4 => &[Key::Ctrl('4')],
79 | Action::FilterIpv6 => &[Key::Ctrl('6')],
80 | Action::FilterIsos => &[Key::Ctrl('5')],
81 | }
82 | }
83 | }
84 |
85 | impl Display for Action {
86 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 | let str = match self {
88 | Action::ClosePopUp => "close popup",
89 | Action::Quit => "quit",
90 | Action::ShowInput => "toggle filter",
91 | Action::NavigateUp => "up",
92 | Action::NavigateDown => "down",
93 | Action::FilterHttps => "toggle https",
94 | Action::FilterHttp => "toggle http",
95 | Action::FilterRsync => "toggle rsync",
96 | Action::FilterFtp => "toggle ftp",
97 | Action::FilterSyncing => "toggle in-sync",
98 | Action::ViewSortAlphabetically => "sort [country] A-Z",
99 | Action::ViewSortMirrorCount => "sort [country] mirrors",
100 | Action::ToggleSelect => "[de]select mirror",
101 | Action::SelectionSortCompletionPct => "sort [selection] completion",
102 | Action::SelectionSortDelay => "sort [selection] delay",
103 | Action::SelectionSortDuration => "sort [selection] duration",
104 | Action::SelectionSortScore => "sort [selection] score",
105 | Action::Export => "export mirrors",
106 | Action::FilterIpv4 => "toggle ipv4",
107 | Action::FilterIpv6 => "toggle ipv6",
108 | Action::FilterIsos => "toggle isos",
109 | };
110 | write!(f, "{str}")
111 | }
112 | }
113 |
114 | /// The application should have some contextual actions.
115 | #[derive(Default, Debug, Clone)]
116 | pub struct Actions(Vec);
117 |
118 | impl Actions {
119 | /// Given a key, find the corresponding action
120 | pub fn find(&self, key: Key) -> Option<&Action> {
121 | Action::iterator()
122 | .filter(|action| self.0.contains(action))
123 | .find(|action| action.keys().contains(&key))
124 | }
125 |
126 | /// Get contextual actions.
127 | /// (just for building a help view)
128 | pub fn actions(&self) -> &[Action] {
129 | self.0.as_slice()
130 | }
131 | }
132 |
133 | impl From> for Actions {
134 | /// Build contextual action
135 | ///
136 | /// # Panics
137 | ///
138 | /// If two actions have same key
139 | fn from(actions: Vec) -> Self {
140 | // Check key unicity
141 | let mut map: HashMap> = HashMap::new();
142 | for action in actions.iter() {
143 | for key in action.keys().iter() {
144 | match map.get_mut(key) {
145 | Some(vec) => vec.push(*action),
146 | None => {
147 | map.insert(*key, vec![*action]);
148 | }
149 | }
150 | }
151 | }
152 | let errors = map
153 | .iter()
154 | .filter(|(_, actions)| actions.len() > 1) // at least two actions share same shortcut
155 | .map(|(key, actions)| {
156 | let actions = actions
157 | .iter()
158 | .map(Action::to_string)
159 | .collect::>()
160 | .join(", ");
161 | format!("Conflict key {key} with actions {actions}")
162 | })
163 | .collect::>();
164 | if !errors.is_empty() {
165 | let err = errors.join("; ");
166 | panic!("{err}")
167 | }
168 |
169 | // Ok, we can create contextual actions
170 | Self(actions)
171 | }
172 | }
173 |
174 | #[cfg(test)]
175 | mod tests {
176 | use super::*;
177 |
178 | #[test]
179 | fn should_find_action_by_key() {
180 | let actions: Actions = vec![Action::Quit, Action::ClosePopUp].into();
181 | let result = actions.find(Key::Ctrl('c'));
182 | assert_eq!(result, Some(&Action::Quit));
183 | }
184 |
185 | #[test]
186 | fn should_find_action_by_key_not_found() {
187 | let actions: Actions = vec![Action::Quit, Action::ClosePopUp].into();
188 | let result = actions.find(Key::Alt('w'));
189 | assert_eq!(result, None);
190 | }
191 |
192 | #[test]
193 | fn should_create_actions_from_vec() {
194 | let _actions: Actions = vec![Action::Quit, Action::ClosePopUp].into();
195 | }
196 |
197 | #[test]
198 | #[should_panic]
199 | fn should_panic_when_create_actions_conflict_key() {
200 | let _actions: Actions = vec![Action::Quit, Action::ClosePopUp, Action::Quit].into();
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/inputs/event.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | sync::{
3 | atomic::{AtomicBool, Ordering},
4 | Arc,
5 | },
6 | time::Duration,
7 | };
8 |
9 | use tracing::error;
10 |
11 | use super::key::Key;
12 | use super::InputEvent;
13 |
14 | /// A small event handler that wrap crossterm input and tick event. Each event
15 | /// type is handled in its own thread and returned to a common `Receiver`
16 | pub struct Events {
17 | rx: tokio::sync::mpsc::Receiver,
18 | // Need to be kept around to prevent disposing the sender side.
19 | _tx: tokio::sync::mpsc::Sender,
20 | // To stop the loop
21 | stop_capture: Arc,
22 | }
23 |
24 | impl Events {
25 | /// Constructs an new instance of `Events` with the default config.
26 | pub fn new(tick_rate: Duration) -> Events {
27 | let (tx, rx) = tokio::sync::mpsc::channel(100);
28 | let stop_capture = Arc::new(AtomicBool::new(false));
29 |
30 | let event_tx = tx.clone();
31 | let event_stop_capture = stop_capture.clone();
32 | tokio::spawn(async move {
33 | loop {
34 | // poll for tick rate duration, if no event, sent tick event.
35 | if crossterm::event::poll(tick_rate).unwrap() {
36 | if let crossterm::event::Event::Key(key) = crossterm::event::read().unwrap() {
37 | let key = Key::from(key);
38 | if let Err(err) = event_tx.send(InputEvent::Input(key)).await {
39 | error!("{err}");
40 | }
41 | }
42 | }
43 | if let Err(err) = event_tx.send(InputEvent::Tick).await {
44 | error!("{err}");
45 | }
46 | if event_stop_capture.load(Ordering::Relaxed) {
47 | break;
48 | }
49 | }
50 | });
51 |
52 | Events {
53 | rx,
54 | _tx: tx,
55 | stop_capture,
56 | }
57 | }
58 |
59 | /// Attempts to read an event.
60 | pub async fn next(&mut self) -> InputEvent {
61 | self.rx.recv().await.unwrap_or(InputEvent::Tick)
62 | }
63 |
64 | /// Close
65 | pub fn close(&mut self) {
66 | self.stop_capture.store(true, Ordering::Relaxed)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/inputs/key.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{self, Display, Formatter};
2 |
3 | use crossterm::event;
4 |
5 | /// Represents an key.
6 | #[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
7 | pub enum Key {
8 | /// Both Enter (or Return) and numpad Enter
9 | Enter,
10 | /// Tabulation key
11 | Tab,
12 | /// Backspace key
13 | Backspace,
14 | /// Escape key
15 | Esc,
16 |
17 | /// Left arrow
18 | Left,
19 | /// Right arrow
20 | Right,
21 | /// Up arrow
22 | Up,
23 | /// Down arrow
24 | Down,
25 |
26 | /// Insert key
27 | Ins,
28 | /// Delete key
29 | Delete,
30 | /// Home key
31 | Home,
32 | /// End key
33 | End,
34 | /// Page Up key
35 | PageUp,
36 | /// Page Down key
37 | PageDown,
38 |
39 | /// F0 key
40 | F0,
41 | /// F1 key
42 | F1,
43 | /// F2 key
44 | F2,
45 | /// F3 key
46 | F3,
47 | /// F4 key
48 | F4,
49 | /// F5 key
50 | F5,
51 | /// F6 key
52 | F6,
53 | /// F7 key
54 | F7,
55 | /// F8 key
56 | F8,
57 | /// F9 key
58 | F9,
59 | /// F10 key
60 | F10,
61 | /// F11 key
62 | F11,
63 | /// F12 key
64 | F12,
65 | Char(char),
66 | Ctrl(char),
67 | Alt(char),
68 | Unknown,
69 | }
70 |
71 | impl Key {
72 | /// If exit
73 | pub fn is_exit(&self) -> bool {
74 | matches!(self, Key::Ctrl('c') | Key::Char('q') | Key::Esc)
75 | }
76 |
77 | /// Returns the function key corresponding to the given number
78 | ///
79 | /// 1 -> F1, etc...
80 | ///
81 | /// # Panics
82 | ///
83 | /// If `n == 0 || n > 12`
84 | pub fn from_f(n: u8) -> Key {
85 | match n {
86 | 0 => Key::F0,
87 | 1 => Key::F1,
88 | 2 => Key::F2,
89 | 3 => Key::F3,
90 | 4 => Key::F4,
91 | 5 => Key::F5,
92 | 6 => Key::F6,
93 | 7 => Key::F7,
94 | 8 => Key::F8,
95 | 9 => Key::F9,
96 | 10 => Key::F10,
97 | 11 => Key::F11,
98 | 12 => Key::F12,
99 | _ => panic!("unknown function key: F{n}"),
100 | }
101 | }
102 | }
103 |
104 | impl Display for Key {
105 | fn fmt(&self, f: &mut Formatter) -> fmt::Result {
106 | match *self {
107 | Key::Alt(' ') => write!(f, "alt+Space"),
108 | Key::Ctrl(' ') => write!(f, "ctrl+Space"),
109 | Key::Char(' ') => write!(f, "space"),
110 | Key::Alt(c) => write!(f, "alt+{c}"),
111 | Key::Ctrl(c) => write!(f, "ctrl+{c}"),
112 | Key::Char(c) => write!(f, "{c}"),
113 | _ => write!(f, "{self:?}"),
114 | }
115 | }
116 | }
117 |
118 | impl From for Key {
119 | fn from(key_event: event::KeyEvent) -> Self {
120 | match key_event {
121 | event::KeyEvent {
122 | code: event::KeyCode::Esc,
123 | ..
124 | } => Key::Esc,
125 | event::KeyEvent {
126 | code: event::KeyCode::Backspace,
127 | ..
128 | } => Key::Backspace,
129 | event::KeyEvent {
130 | code: event::KeyCode::Left,
131 | ..
132 | } => Key::Left,
133 | event::KeyEvent {
134 | code: event::KeyCode::Right,
135 | ..
136 | } => Key::Right,
137 | event::KeyEvent {
138 | code: event::KeyCode::Up,
139 | ..
140 | } => Key::Up,
141 | event::KeyEvent {
142 | code: event::KeyCode::Down,
143 | ..
144 | } => Key::Down,
145 | event::KeyEvent {
146 | code: event::KeyCode::Home,
147 | ..
148 | } => Key::Home,
149 | event::KeyEvent {
150 | code: event::KeyCode::End,
151 | ..
152 | } => Key::End,
153 | event::KeyEvent {
154 | code: event::KeyCode::PageUp,
155 | ..
156 | } => Key::PageUp,
157 | event::KeyEvent {
158 | code: event::KeyCode::PageDown,
159 | ..
160 | } => Key::PageDown,
161 | event::KeyEvent {
162 | code: event::KeyCode::Delete,
163 | ..
164 | } => Key::Delete,
165 | event::KeyEvent {
166 | code: event::KeyCode::Insert,
167 | ..
168 | } => Key::Ins,
169 | event::KeyEvent {
170 | code: event::KeyCode::F(n),
171 | ..
172 | } => Key::from_f(n),
173 | event::KeyEvent {
174 | code: event::KeyCode::Enter,
175 | ..
176 | } => Key::Enter,
177 | event::KeyEvent {
178 | code: event::KeyCode::Tab,
179 | ..
180 | } => Key::Tab,
181 |
182 | // First check for char + modifier
183 | event::KeyEvent {
184 | code: event::KeyCode::Char(c),
185 | modifiers: event::KeyModifiers::ALT,
186 | ..
187 | } => Key::Alt(c),
188 | event::KeyEvent {
189 | code: event::KeyCode::Char(c),
190 | modifiers: event::KeyModifiers::CONTROL,
191 | ..
192 | } => Key::Ctrl(c),
193 |
194 | event::KeyEvent {
195 | code: event::KeyCode::Char(c),
196 | ..
197 | } => Key::Char(c),
198 |
199 | _ => Key::Unknown,
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/inputs/mod.rs:
--------------------------------------------------------------------------------
1 | use self::key::Key;
2 |
3 | pub mod event;
4 | pub mod key;
5 |
6 | pub enum InputEvent {
7 | Input(Key),
8 | Tick,
9 | }
10 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/io/handler.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{bail, Result};
2 |
3 | use archlinux::{
4 | chrono::{DateTime, Utc},
5 | ArchLinux, Client, Country,
6 | };
7 |
8 | use std::{
9 | path::PathBuf,
10 | sync::{atomic::AtomicBool, Arc},
11 | time::SystemTime,
12 | };
13 |
14 | use itertools::Itertools;
15 | use tokio::sync::Mutex;
16 | use tracing::{error, info, warn};
17 |
18 | use crate::{
19 | config::Configuration,
20 | tui::state::{App, PopUpState},
21 | };
22 |
23 | use super::IoEvent;
24 |
25 | const CACHE_FILE: &str = "cache";
26 |
27 | pub struct IoAsyncHandler {
28 | app: Arc>,
29 | popup: Arc>,
30 | client: Client,
31 | }
32 |
33 | impl IoAsyncHandler {
34 | pub fn new(app: Arc>, popup: Arc>, client: Client) -> Self {
35 | Self { app, popup, client }
36 | }
37 |
38 | pub async fn initialise(&mut self, config: Arc>) -> Result<()> {
39 | let (is_fresh, cache_file) = is_fresh(Arc::clone(&config)).await;
40 | if is_fresh {
41 | match tokio::fs::read_to_string(cache_file.as_ref().unwrap()).await {
42 | Ok(contents) => {
43 | let result = archlinux::parse_local(&contents);
44 | match result {
45 | Ok(mirrors) => {
46 | show_stats(&mirrors.countries, is_fresh);
47 |
48 | update_state(Arc::clone(&self.app), Arc::clone(&config), mirrors).await;
49 | }
50 | Err(e) => {
51 | if let Err(f) = get_new_mirrors(
52 | cache_file,
53 | Arc::clone(&self.app),
54 | Arc::clone(&config),
55 | self.client.clone(),
56 | )
57 | .await
58 | {
59 | error!("{e}, {f}");
60 | }
61 | }
62 | }
63 | }
64 | Err(e) => {
65 | error!("{e}");
66 | if let Err(e) = get_new_mirrors(
67 | cache_file,
68 | Arc::clone(&self.app),
69 | Arc::clone(&config),
70 | self.client.clone(),
71 | )
72 | .await
73 | {
74 | error!("{e}");
75 | }
76 | }
77 | }
78 | // read cached
79 | } else if let Err(e) = get_new_mirrors(
80 | cache_file,
81 | Arc::clone(&self.app),
82 | Arc::clone(&config),
83 | self.client.clone(),
84 | )
85 | .await
86 | {
87 | error!("{e}");
88 | }
89 | Ok(())
90 | }
91 |
92 | pub async fn close_popup(&self) -> Result<()> {
93 | let mut state = self.popup.lock().await;
94 | state.visible = false;
95 | Ok(())
96 | }
97 |
98 | pub async fn export(
99 | &self,
100 | in_progress: Arc,
101 | progress_transmitter: std::sync::mpsc::Sender,
102 | ) -> Result<()> {
103 | in_progress.store(true, std::sync::atomic::Ordering::Relaxed);
104 |
105 | let mut popup_state = self.popup.lock().await;
106 | popup_state.popup_text = String::from("Exporting your mirrors, please wait...");
107 | popup_state.visible = true;
108 | std::mem::drop(popup_state);
109 |
110 | let (check_dl_speed, outfile, export_count, mut selected_mirrors, extra_urls, age) = {
111 | let app_state = self.app.lock().await;
112 | let configuration = app_state.configuration.lock().unwrap();
113 | let check_dl_speed = configuration.rate;
114 | let outfile = configuration.outfile.clone();
115 | let export_count = configuration.export as usize;
116 | let include = configuration.include.clone();
117 | let age = configuration.age;
118 |
119 | let selected_mirrors = app_state
120 | .selected_mirrors
121 | .iter()
122 | .map(|f| f.url.to_owned())
123 | .collect_vec();
124 | (
125 | check_dl_speed,
126 | outfile,
127 | export_count,
128 | selected_mirrors,
129 | include,
130 | age,
131 | )
132 | };
133 |
134 | let client = self.client.clone();
135 | let included_urls = tokio::spawn(async move {
136 | if let Some(extra_urls) = extra_urls {
137 | let results = check_extra_urls(extra_urls, age, client).await;
138 | Some(results)
139 | } else {
140 | None
141 | }
142 | });
143 |
144 | if let Ok(Some(Ok(mut item))) = included_urls.await {
145 | selected_mirrors.append(&mut item)
146 | }
147 |
148 | if !check_dl_speed {
149 | Self::write_to_file(
150 | outfile,
151 | &selected_mirrors,
152 | export_count,
153 | Some(in_progress),
154 | Some(Arc::clone(&self.popup)),
155 | )
156 | .await;
157 | } else {
158 | Self::rate_mirrors(
159 | selected_mirrors,
160 | Some(Arc::clone(&self.popup)),
161 | Some(progress_transmitter),
162 | outfile,
163 | export_count,
164 | Some(in_progress),
165 | self.client.clone(),
166 | )
167 | .await;
168 | }
169 |
170 | Ok(())
171 | }
172 |
173 | pub async fn rate_mirrors(
174 | selected_mirrors: Vec,
175 | popup: Option>>,
176 | progress_transmitter: Option>,
177 | outfile: PathBuf,
178 | export_count: usize,
179 | in_progress: Option>,
180 | client: Client,
181 | ) -> tokio::task::JoinHandle<()> {
182 | let mut mirrors = Vec::with_capacity(selected_mirrors.len());
183 |
184 | let mut set = tokio::task::JoinSet::new();
185 |
186 | for i in selected_mirrors.iter() {
187 | set.spawn(archlinux::rate_mirror(i.clone(), client.clone()));
188 | }
189 |
190 | let popup_state = popup.clone();
191 |
192 | tokio::spawn(async move {
193 | let mut current = 0;
194 | let len = set.len();
195 |
196 | while let Some(res) = set.join_next().await {
197 | match res {
198 | Ok(Ok((duration, url))) => {
199 | mirrors.push((duration, url));
200 | }
201 | Ok(Err(cause)) => match cause {
202 | archlinux::Error::Connection(e) => {
203 | error!("{e}");
204 | }
205 | archlinux::Error::Parse(e) => {
206 | error!("{e}");
207 | }
208 | archlinux::Error::Rate {
209 | qualified_url,
210 | url,
211 | status_code,
212 | } => {
213 | error!(
214 | "could not locate {qualified_url} from {url}, reason=> {status_code}",
215 | );
216 | }
217 | archlinux::Error::Request(e) => {
218 | error!("{e}");
219 | }
220 | archlinux::Error::TimeError(e) => {
221 | error!("{e}")
222 | }
223 | },
224 | Err(e) => error!("{e}"),
225 | }
226 | if let Some(progress_transmitter) = progress_transmitter.as_ref() {
227 | current += 1;
228 | let value = (current as f32) / (len as f32) * 100.0;
229 | let _ = progress_transmitter.send(value);
230 | }
231 | }
232 |
233 | let results = {
234 | if !mirrors.is_empty() {
235 | mirrors.sort_by(|(duration_a, _), (duration_b, _)| duration_a.cmp(duration_b));
236 |
237 | mirrors.iter().map(|(_, url)| url.to_owned()).collect_vec()
238 | } else {
239 | warn!("Exporting mirrors without rating...");
240 | selected_mirrors.to_vec()
241 | }
242 | };
243 |
244 | Self::write_to_file(outfile, &results, export_count, in_progress, popup_state).await;
245 |
246 | if let Some(progress) = progress_transmitter {
247 | let _ = progress.send(0.0); // reset progress
248 | }
249 | })
250 | }
251 |
252 | pub async fn write_to_file(
253 | outfile: PathBuf,
254 | selected_mirrors: &[String],
255 | export_count: usize,
256 | in_progress: Option>,
257 | popup: Option>>,
258 | ) {
259 | if let Some(dir) = outfile.parent() {
260 | info!(count = %export_count, "making export of mirrors");
261 | if tokio::fs::create_dir_all(dir).await.is_ok() {
262 | let output = &selected_mirrors[if selected_mirrors.len() >= export_count {
263 | ..export_count
264 | } else {
265 | ..selected_mirrors.len()
266 | }];
267 | let output: Vec<_> = output
268 | .iter()
269 | .map(|f| format!("Server = {f}$repo/os/$arch"))
270 | .collect();
271 |
272 | if let Err(e) = tokio::fs::write(&outfile, output.join("\n")).await {
273 | error!("{e}");
274 | } else {
275 | info!("Your mirrorlist has been exported");
276 | }
277 | if let Some(popup) = popup {
278 | let mut state = popup.lock().await;
279 | state.popup_text = format!(
280 | "Your mirrorlist has been successfully exported to: {}",
281 | outfile.display()
282 | );
283 | }
284 | }
285 | }
286 | if let Some(in_progress) = in_progress {
287 | in_progress.store(false, std::sync::atomic::Ordering::Relaxed);
288 | }
289 | }
290 |
291 | pub async fn handle_io_event(
292 | &mut self,
293 | io_event: IoEvent,
294 | config: Arc>,
295 | ) {
296 | if let Err(e) = match io_event {
297 | IoEvent::Initialise => {
298 | if let Err(e) = self.initialise(config).await {
299 | error!("{e}")
300 | };
301 | let mut popup = self.popup.lock().await;
302 | popup.visible = false;
303 | Ok(())
304 | }
305 | IoEvent::ClosePopUp => self.close_popup().await,
306 | IoEvent::Export {
307 | in_progress,
308 | progress_transmitter,
309 | } => self.export(in_progress, progress_transmitter).await,
310 | } {
311 | error!("{e}");
312 | }
313 | let mut app = self.app.lock().await;
314 | app.ready();
315 | }
316 | }
317 |
318 | async fn check_extra_urls(
319 | extra_urls: Vec,
320 | age: u16,
321 | client: Client,
322 | ) -> Result> {
323 | info!("parsing included URLs");
324 | let mut results = Vec::with_capacity(extra_urls.len());
325 |
326 | let mut set = tokio::task::JoinSet::new();
327 |
328 | for i in extra_urls.into_iter() {
329 | set.spawn(archlinux::get_last_sync(i, client.clone()));
330 | }
331 |
332 | while let Some(res) = set.join_next().await {
333 | match res {
334 | Ok(Ok((dt, url))) => {
335 | let utc: DateTime = Utc::now();
336 | let diff = utc - dt;
337 | if i64::from(age) >= diff.num_hours() {
338 | results.push(url);
339 | }
340 | }
341 | Ok(Err(e)) => {
342 | error!("{e}")
343 | }
344 | Err(e) => {
345 | error!("{e}")
346 | }
347 | }
348 | }
349 |
350 | Ok(results)
351 | }
352 |
353 | // Do we get a new mirrorlist or nah
354 | pub async fn is_fresh(
355 | app: Arc>,
356 | ) -> (bool, Option) {
357 | if let Some(mut cache) = dirs::cache_dir() {
358 | let crate_name = env!("CARGO_PKG_NAME");
359 | cache.push(crate_name);
360 | if let Err(e) = tokio::fs::create_dir_all(&cache).await {
361 | error!("could not create cache directory, {e}");
362 | }
363 | cache.push(CACHE_FILE);
364 | if cache.exists() {
365 | let config = app.lock().unwrap();
366 | let expires = config.ttl;
367 | drop(config);
368 |
369 | let duration = cache.metadata().map(|f| {
370 | f.modified().map(|f| {
371 | let now = SystemTime::now();
372 | now.duration_since(f)
373 | })
374 | });
375 | match duration {
376 | Ok(Ok(Ok(duration))) => {
377 | let hours = duration.as_secs() / 3600;
378 | if hours < expires as u64 {
379 | (true, Some(cache))
380 | } else {
381 | (false, Some(cache))
382 | }
383 | }
384 | _ => (false, Some(cache)),
385 | }
386 | } else {
387 | (false, Some(cache))
388 | }
389 | } else {
390 | (false, None)
391 | }
392 | }
393 |
394 | async fn get_new_mirrors(
395 | cache_file: Option,
396 | app: Arc>,
397 | config: Arc>,
398 | client: Client,
399 | ) -> Result<()> {
400 | let url = Arc::new(Mutex::new(String::default()));
401 | let inner = Arc::clone(&url);
402 | {
403 | let mut val = inner.lock().await;
404 | let source = config.lock().unwrap();
405 | *val = source.url.clone();
406 | };
407 | let strs = url.lock().await;
408 |
409 | match archlinux::get_mirrors_with_client(&strs, client).await {
410 | Ok((mirrors, str_value)) => {
411 | if let Some(cache) = cache_file {
412 | if let Err(e) = tokio::fs::write(cache, str_value).await {
413 | error!("{e}");
414 | }
415 | }
416 |
417 | show_stats(&mirrors.countries, false);
418 |
419 | let mut app = app.lock().await;
420 | app.mirrors = Some(mirrors);
421 | }
422 | Err(e) => {
423 | warn!("{e}, using old cached file fallback");
424 | if let Some(file) = cache_file {
425 | let slice = tokio::fs::read_to_string(file).await;
426 |
427 | match slice.ok().and_then(|f| archlinux::parse_local(&f).ok()) {
428 | Some(mirrors) => {
429 | update_state(app, Arc::clone(&config), mirrors).await;
430 | }
431 | _ => {
432 | bail!("{e}");
433 | }
434 | }
435 | }
436 | }
437 | }
438 | Ok(())
439 | }
440 |
441 | async fn update_state(
442 | app: Arc>,
443 | config: Arc>,
444 | mut mirrors: ArchLinux,
445 | ) {
446 | let mut app = app.lock().await;
447 | let config = config.lock().unwrap();
448 | if !config.country.is_empty() {
449 | let items = mirrors
450 | .countries
451 | .into_iter()
452 | .filter(|f| {
453 | config
454 | .country
455 | .iter()
456 | .any(|a| a.eq_ignore_ascii_case(&f.name))
457 | })
458 | .collect_vec();
459 | mirrors.countries = items;
460 | }
461 | app.mirrors = Some(mirrors);
462 | }
463 |
464 | fn show_stats(slice: &[Country], is_cache: bool) {
465 | let mut count = 0;
466 | for i in slice.iter() {
467 | count += i.mirrors.len();
468 | }
469 | info!(
470 | "Found {count} mirrors from {} countries{}.",
471 | slice.len(),
472 | if is_cache { " cached" } else { "" }
473 | );
474 | }
475 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/io/mod.rs:
--------------------------------------------------------------------------------
1 | use std::sync::{atomic::AtomicBool, mpsc::Sender, Arc};
2 |
3 | pub mod handler;
4 |
5 | pub enum IoEvent {
6 | Initialise,
7 | ClosePopUp,
8 | Export {
9 | in_progress: Arc,
10 | progress_transmitter: Sender,
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/mod.rs:
--------------------------------------------------------------------------------
1 | mod actions;
2 | mod inputs;
3 | pub mod io;
4 | mod state;
5 | mod ui;
6 | pub mod view;
7 |
8 | use anyhow::Result;
9 |
10 | use archlinux::get_client;
11 | use crossterm::{
12 | event::{DisableMouseCapture, EnableMouseCapture},
13 | execute,
14 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
15 | };
16 | use std::{
17 | sync::{atomic::AtomicBool, Arc},
18 | time::Duration,
19 | };
20 | use tokio::sync::Mutex;
21 | use tracing::debug;
22 |
23 | use ratatui::{backend::CrosstermBackend, Terminal};
24 |
25 | use crate::config::Configuration;
26 |
27 | use self::{
28 | inputs::{event::Events, InputEvent},
29 | io::{handler::IoAsyncHandler, IoEvent},
30 | state::{App, AppReturn, PopUpState},
31 | ui::ui,
32 | };
33 |
34 | pub async fn start(configuration: Arc>) -> Result<()> {
35 | enable_raw_mode()?;
36 | let mut stdout = std::io::stdout();
37 |
38 | let client = {
39 | let timeout = configuration.lock().unwrap();
40 | let timeout = timeout.connection_timeout;
41 | get_client(timeout)?
42 | };
43 |
44 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
45 |
46 | let backend = CrosstermBackend::new(stdout);
47 | let mut terminal = Terminal::new(backend)?;
48 | let (sync_io_tx, mut sync_io_rx) = tokio::sync::mpsc::channel::(100);
49 | let app = Arc::new(Mutex::new(App::new(sync_io_tx, Arc::clone(&configuration))));
50 | let inner = Arc::clone(&app);
51 |
52 | let popup_state = Arc::new(Mutex::new(PopUpState::new()));
53 | {
54 | let popup_state = Arc::clone(&popup_state);
55 | tokio::spawn(async move {
56 | let mut handler = IoAsyncHandler::new(inner, popup_state, client);
57 | debug!("Getting Arch Linux mirrors. Please wait");
58 | while let Some(io_event) = sync_io_rx.recv().await {
59 | handler
60 | .handle_io_event(io_event, Arc::clone(&configuration))
61 | .await;
62 | }
63 | });
64 | }
65 | let res = run_app(&mut terminal, Arc::clone(&app), popup_state).await;
66 |
67 | disable_raw_mode()?;
68 | execute!(
69 | terminal.backend_mut(),
70 | LeaveAlternateScreen,
71 | DisableMouseCapture
72 | )?;
73 | terminal.show_cursor()?;
74 |
75 | if let Err(err) = res {
76 | eprintln!("{err:?}")
77 | }
78 |
79 | Ok(())
80 | }
81 |
82 | async fn run_app(
83 | terminal: &mut Terminal>,
84 | app: Arc>,
85 | popup_state: Arc>,
86 | ) -> std::io::Result<()> {
87 | let tick_rate = Duration::from_millis(100);
88 | let mut events = Events::new(tick_rate);
89 |
90 | // Trigger state change from Init to Initialised
91 | {
92 | let mut app = app.lock().await;
93 | // Here we assume the the first load is a long task
94 | app.dispatch(IoEvent::Initialise).await;
95 | }
96 |
97 | let exporting = Arc::new(AtomicBool::new(false));
98 | let (pos_tx, pos_rx) = std::sync::mpsc::channel();
99 |
100 | loop {
101 | let mut app = app.lock().await;
102 | let popup = popup_state.lock().await;
103 |
104 | terminal.draw(|f| ui(f, &mut app, &popup, Arc::clone(&exporting), &pos_rx))?;
105 |
106 | let result = match events.next().await {
107 | InputEvent::Input(key) => {
108 | app.dispatch_action(key, Arc::clone(&exporting), pos_tx.clone())
109 | .await
110 | }
111 | InputEvent::Tick => app.update_on_tick().await,
112 | };
113 |
114 | if result == AppReturn::Exit {
115 | events.close();
116 | break;
117 | }
118 | }
119 | Ok(())
120 | }
121 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/state.rs:
--------------------------------------------------------------------------------
1 | use archlinux::{
2 | chrono::{DateTime, Utc},
3 | ArchLinux, Country,
4 | };
5 | use std::sync::{atomic::AtomicBool, mpsc::Sender, Arc, Mutex};
6 |
7 | use crate::{
8 | cli::{Protocol, ViewSort},
9 | config::Configuration,
10 | };
11 |
12 | use itertools::Itertools;
13 | use ratatui::{
14 | style::{Color, Modifier, Style},
15 | widgets::{Cell, Row},
16 | };
17 | use tracing::{error, info, warn};
18 | use unicode_width::UnicodeWidthStr;
19 |
20 | use crate::tui::actions::Action;
21 |
22 | use super::{actions::Actions, inputs::key::Key, io::IoEvent, ui::filter_result};
23 |
24 | #[derive(Debug, PartialEq, Eq)]
25 | pub enum AppReturn {
26 | Exit,
27 | Continue,
28 | }
29 |
30 | pub struct App {
31 | pub actions: Actions,
32 | pub mirrors: Option,
33 | pub io_tx: tokio::sync::mpsc::Sender,
34 | pub input: String,
35 | pub input_cursor_position: usize,
36 | pub show_input: bool,
37 | pub scroll_pos: isize,
38 | pub filtered_countries: Vec<(Country, usize)>,
39 | pub selected_mirrors: Vec,
40 | pub table_viewport_height: u16,
41 | pub configuration: Arc>,
42 | pub show_insync: bool,
43 | }
44 |
45 | pub struct PopUpState {
46 | pub popup_text: String,
47 | pub visible: bool,
48 | }
49 |
50 | impl PopUpState {
51 | pub fn new() -> Self {
52 | Self {
53 | popup_text: String::from("Getting mirrors... please wait..."),
54 | visible: true,
55 | }
56 | }
57 | }
58 |
59 | #[derive(Debug, Clone)]
60 | pub struct SelectedMirror {
61 | pub country_code: String,
62 | pub protocol: Protocol,
63 | pub completion_pct: f32,
64 | pub delay: Option,
65 | pub score: Option,
66 | pub duration_stddev: Option,
67 | pub last_sync: Option>,
68 | pub url: String,
69 | }
70 |
71 | impl App {
72 | pub fn new(
73 | io_tx: tokio::sync::mpsc::Sender,
74 | configuration: Arc>,
75 | ) -> Self {
76 | let show_sync = configuration.lock().unwrap();
77 | let sync = show_sync.age != 0;
78 | drop(show_sync);
79 | Self {
80 | actions: vec![Action::Quit].into(),
81 | show_input: false,
82 | mirrors: None,
83 | io_tx,
84 | input: String::default(),
85 | input_cursor_position: 0,
86 | configuration,
87 | scroll_pos: 0,
88 | table_viewport_height: 0,
89 | selected_mirrors: vec![],
90 | filtered_countries: vec![],
91 | show_insync: sync,
92 | }
93 | }
94 |
95 | pub async fn dispatch_action(
96 | &mut self,
97 | key: Key,
98 | exporting: Arc,
99 | progress_transmitter: Sender,
100 | ) -> AppReturn {
101 | if let Some(action) = self.actions.find(key) {
102 | if key.is_exit() && !self.show_input {
103 | AppReturn::Exit
104 | } else if self.show_input {
105 | match action {
106 | Action::Quit => {
107 | if key == Key::Char('q') {
108 | insert_character(self, 'q');
109 | }
110 | }
111 | Action::NavigateUp => {
112 | if key == Key::Char('k') {
113 | insert_character(self, 'k');
114 | }
115 | }
116 | Action::NavigateDown => {
117 | if key == Key::Char('j') {
118 | insert_character(self, 'j');
119 | }
120 | }
121 | Action::ViewSortAlphabetically => insert_character(self, '1'),
122 | Action::ViewSortMirrorCount => insert_character(self, '2'),
123 | _ => {}
124 | }
125 | AppReturn::Continue
126 | } else {
127 | match action {
128 | Action::ClosePopUp => {
129 | let _ = self.io_tx.send(IoEvent::ClosePopUp).await;
130 | AppReturn::Continue
131 | }
132 | Action::Quit => AppReturn::Continue,
133 | Action::ShowInput => {
134 | self.show_input = !self.show_input;
135 | AppReturn::Continue
136 | }
137 | Action::NavigateUp => {
138 | self.previous();
139 | AppReturn::Continue
140 | }
141 | Action::NavigateDown => {
142 | self.next();
143 | AppReturn::Continue
144 | }
145 | Action::FilterHttps => insert_filter(self, Protocol::Https),
146 | Action::FilterHttp => insert_filter(self, Protocol::Http),
147 | Action::FilterRsync => insert_filter(self, Protocol::Rsync),
148 | Action::FilterFtp => insert_filter(self, Protocol::Ftp),
149 | Action::FilterSyncing => insert_filter(self, Protocol::InSync),
150 | Action::ViewSortAlphabetically => insert_sort(self, ViewSort::Alphabetical),
151 | Action::ViewSortMirrorCount => insert_sort(self, ViewSort::MirrorCount),
152 | Action::ToggleSelect => {
153 | self.focused_country();
154 | AppReturn::Continue
155 | }
156 | Action::SelectionSortCompletionPct => {
157 | self.selected_mirrors
158 | .sort_by(|a, b| b.completion_pct.total_cmp(&a.completion_pct));
159 | AppReturn::Continue
160 | }
161 | Action::SelectionSortDelay => {
162 | self.selected_mirrors.sort_by(|a, b| {
163 | let a = a.delay.unwrap_or(i64::MAX);
164 | let b = b.delay.unwrap_or(i64::MAX);
165 | a.partial_cmp(&b).unwrap()
166 | });
167 | AppReturn::Continue
168 | }
169 | Action::SelectionSortScore => {
170 | self.selected_mirrors.sort_by(|a, b| {
171 | let a = a.score.unwrap_or(f64::MAX);
172 | let b = b.score.unwrap_or(f64::MAX);
173 | a.partial_cmp(&b).unwrap()
174 | });
175 | AppReturn::Continue
176 | }
177 | Action::SelectionSortDuration => {
178 | self.selected_mirrors.sort_by(|a, b| {
179 | let a = a.duration_stddev.unwrap_or(f64::MAX);
180 | let b = b.duration_stddev.unwrap_or(f64::MAX);
181 | a.partial_cmp(&b).unwrap()
182 | });
183 | AppReturn::Continue
184 | }
185 | Action::Export => {
186 | if !exporting.load(std::sync::atomic::Ordering::Relaxed) {
187 | if self.selected_mirrors.is_empty() {
188 | warn!("You haven't selected any mirrors yet");
189 | } else {
190 | let _ = self
191 | .io_tx
192 | .send(IoEvent::Export {
193 | in_progress: Arc::clone(&exporting),
194 | progress_transmitter,
195 | })
196 | .await;
197 | }
198 | }
199 | AppReturn::Continue
200 | }
201 | Action::FilterIpv4 => insert_filter(self, Protocol::Ipv4),
202 | Action::FilterIpv6 => insert_filter(self, Protocol::Ipv6),
203 | Action::FilterIsos => insert_filter(self, Protocol::Isos),
204 | }
205 | }
206 | } else {
207 | if self.show_input {
208 | match key {
209 | Key::Backspace => {
210 | if !self.input.is_empty() {
211 | self.input = format!(
212 | "{}{}",
213 | &self.input[..self.input_cursor_position - 1],
214 | &self.input[self.input_cursor_position..]
215 | );
216 | self.input_cursor_position -= 1;
217 | }
218 | }
219 | Key::Left => {
220 | if self.input_cursor_position > 0 {
221 | self.input_cursor_position -= 1;
222 | }
223 | }
224 | Key::Right => {
225 | if self.input_cursor_position < self.input.width() {
226 | self.input_cursor_position += 1;
227 | } else {
228 | self.input_cursor_position = self.input.width();
229 | };
230 | }
231 | Key::Delete => {
232 | if self.input_cursor_position < self.input.width() {
233 | self.input.remove(self.input_cursor_position);
234 | }
235 | }
236 | Key::Home => {
237 | self.input_cursor_position = 0;
238 | }
239 | Key::End => {
240 | self.input_cursor_position = self.input.width();
241 | }
242 | Key::Char(c) => {
243 | insert_character(self, c);
244 | self.scroll_pos = 0;
245 | }
246 | Key::Esc => {
247 | self.show_input = false;
248 | }
249 | _ => {
250 | warn!("No action associated to {key}");
251 | }
252 | }
253 | } else {
254 | warn!("No action associated to {key}");
255 | }
256 | AppReturn::Continue
257 | }
258 | }
259 |
260 | pub async fn dispatch(&mut self, action: IoEvent) {
261 | if let Err(e) = self.io_tx.send(action).await {
262 | error!("Error from dispatch {e}");
263 | };
264 | }
265 |
266 | pub async fn update_on_tick(&mut self) -> AppReturn {
267 | AppReturn::Continue
268 | }
269 |
270 | pub fn ready(&mut self) {
271 | self.actions = vec![
272 | Action::ShowInput,
273 | Action::ClosePopUp,
274 | Action::Quit,
275 | Action::NavigateDown,
276 | Action::NavigateUp,
277 | Action::FilterHttp,
278 | Action::FilterHttps,
279 | Action::FilterFtp,
280 | Action::FilterRsync,
281 | Action::FilterSyncing,
282 | Action::FilterIpv4,
283 | Action::FilterIpv6,
284 | Action::FilterIsos,
285 | Action::ToggleSelect,
286 | Action::ViewSortAlphabetically,
287 | Action::ViewSortMirrorCount,
288 | Action::SelectionSortCompletionPct,
289 | Action::SelectionSortDelay,
290 | Action::SelectionSortDuration,
291 | Action::SelectionSortScore,
292 | Action::Export,
293 | ]
294 | .into();
295 | }
296 |
297 | pub fn next(&mut self) {
298 | if self.scroll_pos + 1 == self.filtered_countries.len() as isize {
299 | self.scroll_pos = 0;
300 | } else {
301 | self.scroll_pos += 1;
302 | }
303 | }
304 |
305 | pub fn previous(&mut self) {
306 | if self.scroll_pos - 1 < 0 {
307 | self.scroll_pos = (self.filtered_countries.len() - 1) as isize;
308 | } else {
309 | self.scroll_pos -= 1;
310 | }
311 | }
312 |
313 | pub fn view_fragments<'a, T>(&'a self, iter: &'a [T]) -> Vec<&'a [T]> {
314 | iter.chunks(self.table_viewport_height.into()).collect_vec()
315 | }
316 |
317 | pub fn rows(&self) -> Vec {
318 | self.filtered_countries
319 | .iter()
320 | .enumerate()
321 | .map(|(idx, (f, count))| {
322 | let c = if idx == self.filtered_countries.len() - 1 {
323 | '╰'
324 | } else {
325 | '├'
326 | };
327 | let mut selected = false;
328 | let default = format!("{c}─ [{}] {}", f.code, f.name);
329 | let item_name = match self.scroll_pos as usize == idx {
330 | true => {
331 | if idx == self.scroll_pos as usize {
332 | selected = true;
333 | format!("{c}─»[{}] {}«", f.code, f.name)
334 | } else {
335 | default
336 | }
337 | }
338 | false => default,
339 | };
340 |
341 | let index = format!(" {idx}│");
342 |
343 | return Row::new([index, item_name, count.to_string()].iter().map(|c| {
344 | Cell::from(c.clone()).style(if selected {
345 | Style::default()
346 | .add_modifier(Modifier::BOLD)
347 | .fg(Color::Green)
348 | } else {
349 | Style::default().fg(Color::Gray)
350 | })
351 | }));
352 | })
353 | .collect_vec()
354 | }
355 |
356 | pub fn view(&self, fragment: &[T]) -> T {
357 | fragment[self.fragment_number()]
358 | }
359 |
360 | pub fn focused_country(&mut self) {
361 | if self.mirrors.is_some() {
362 | let country = if self.scroll_pos < self.table_viewport_height as isize {
363 | let (country, _) = &self.filtered_countries[self.scroll_pos as usize];
364 | // we can directly index
365 | info!("selected: {}", country.name);
366 | country
367 | } else {
368 | let page = self.fragment_number();
369 | let index = (self.scroll_pos
370 | - (page * self.table_viewport_height as usize) as isize)
371 | as usize;
372 | let fragments = self.view_fragments(&self.filtered_countries);
373 | let frag = fragments[page];
374 | let (country, _) = &frag[index];
375 | info!("selected: {}", country.name);
376 | country
377 | };
378 |
379 | let mut mirrors = country
380 | .mirrors
381 | .iter()
382 | .filter(|f| filter_result(self, f))
383 | .map(|f| SelectedMirror {
384 | country_code: country.code.to_string(),
385 | protocol: Protocol::from(f.protocol),
386 | completion_pct: f.completion_pct,
387 | delay: f.delay,
388 | score: f.score,
389 | duration_stddev: f.duration_stddev,
390 | last_sync: f.last_sync,
391 | url: f.url.to_string(),
392 | })
393 | .collect_vec();
394 |
395 | let pos = self
396 | .selected_mirrors
397 | .iter()
398 | .positions(|f| f.country_code == country.code)
399 | .collect_vec();
400 |
401 | if pos.is_empty() {
402 | self.selected_mirrors.append(&mut mirrors)
403 | } else {
404 | let new_items = self
405 | .selected_mirrors
406 | .iter()
407 | .filter_map(|f| {
408 | if f.country_code != country.code {
409 | Some(f.clone())
410 | } else {
411 | None
412 | }
413 | })
414 | .collect_vec();
415 |
416 | self.selected_mirrors = new_items;
417 | }
418 | }
419 | }
420 |
421 | fn fragment_number(&self) -> usize {
422 | (self.scroll_pos / self.table_viewport_height as isize) as usize
423 | }
424 | }
425 |
426 | fn insert_character(app: &mut App, key: char) {
427 | app.input.insert(app.input_cursor_position, key);
428 | app.input_cursor_position += 1;
429 | app.scroll_pos = 0;
430 | }
431 |
432 | fn insert_filter(app: &mut App, filter: Protocol) -> AppReturn {
433 | let mut config = app.configuration.lock().unwrap();
434 | if let Some(idx) = config.filters.iter().position(|f| *f == filter) {
435 | info!("protocol filter: removed {filter}");
436 | config.filters.remove(idx);
437 | app.show_insync = false;
438 | } else {
439 | info!("protocol filter: added {filter}");
440 | config.filters.push(filter);
441 | app.show_insync = false;
442 | }
443 | app.scroll_pos = 0;
444 | AppReturn::Continue
445 | }
446 |
447 | fn insert_sort(app: &mut App, view: ViewSort) -> AppReturn {
448 | let mut config = app.configuration.lock().unwrap();
449 | config.view = view;
450 | AppReturn::Continue
451 | }
452 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/ui.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | sync::{atomic::AtomicBool, mpsc::Receiver, Arc},
3 | time::Duration,
4 | };
5 |
6 | use archlinux::{
7 | chrono::{DateTime, Local},
8 | Mirror,
9 | };
10 |
11 | use itertools::Itertools;
12 | use ratatui::{
13 | layout::{Alignment, Constraint, Direction, Layout, Rect},
14 | style::{Color, Modifier, Style},
15 | text::{Line, Span},
16 | widgets::{Block, BorderType, Borders, Cell, Clear, Gauge, Paragraph, Row, Table},
17 | Frame,
18 | };
19 | use tracing::debug;
20 | use tui_logger::TuiLoggerWidget;
21 |
22 | use crate::cli::{Protocol, ViewSort};
23 |
24 | use super::{
25 | actions::{Action, Actions},
26 | state::{App, PopUpState},
27 | };
28 |
29 | pub fn ui(
30 | f: &mut Frame,
31 | app: &mut App,
32 | popup: &PopUpState,
33 | exporting: Arc,
34 | percentage: &Receiver,
35 | ) {
36 | const MIN_WIDTH: u16 = 80;
37 | const MIN_HEIGHT: u16 = 27;
38 | let area = f.size();
39 | if check_size(&area, MIN_WIDTH, MIN_HEIGHT) {
40 | let region = Layout::default()
41 | .direction(Direction::Vertical)
42 | .constraints(
43 | [
44 | Constraint::Percentage(25),
45 | Constraint::Percentage(25),
46 | Constraint::Percentage(25),
47 | Constraint::Percentage(25),
48 | ]
49 | .as_ref(),
50 | )
51 | .split(centered_rect(50, 50, area));
52 |
53 | let current_size_label = Line::from(vec![Span::styled(
54 | "Terminal size is too small",
55 | Style::default().add_modifier(Modifier::BOLD),
56 | )]);
57 | let current_size = Line::from(vec![
58 | Span::styled("width = ", Style::default()),
59 | Span::styled(
60 | area.width.to_string(),
61 | if area.width < MIN_WIDTH {
62 | Style::default().fg(Color::Red)
63 | } else {
64 | Style::default().fg(Color::Green)
65 | },
66 | ),
67 | Span::styled(" height = ", Style::default()),
68 | Span::styled(
69 | area.height.to_string(),
70 | if area.height < MIN_HEIGHT {
71 | Style::default().fg(Color::Red)
72 | } else {
73 | Style::default().fg(Color::Green)
74 | },
75 | ),
76 | ]);
77 | let expected_size_label = Line::from(vec![Span::styled(
78 | "Expected size",
79 | Style::default().add_modifier(Modifier::BOLD),
80 | )]);
81 | let expected_size = Line::from(vec![Span::styled(
82 | format!("width = {MIN_WIDTH} height = {MIN_HEIGHT}"),
83 | Style::default(),
84 | )]);
85 | let text = Paragraph::new(current_size_label).alignment(Alignment::Center);
86 | let current_size = Paragraph::new(current_size).alignment(Alignment::Center);
87 | let expected_size_label = Paragraph::new(expected_size_label).alignment(Alignment::Center);
88 | let expected_size = Paragraph::new(expected_size).alignment(Alignment::Center);
89 | f.render_widget(text, region[0]);
90 | f.render_widget(current_size, region[1]);
91 | f.render_widget(expected_size_label, region[2]);
92 | f.render_widget(expected_size, region[3]);
93 | } else {
94 | let chunks = Layout::default()
95 | .constraints([Constraint::Min(20), Constraint::Length(3)].as_ref())
96 | .split(area);
97 |
98 | let body_chunks = Layout::default()
99 | .direction(Direction::Horizontal)
100 | .constraints([Constraint::Min(20), Constraint::Length(60)].as_ref())
101 | .split(chunks[0]);
102 |
103 | {
104 | // Body & Help
105 | let sidebar = Layout::default()
106 | .direction(Direction::Vertical)
107 | .constraints([Constraint::Percentage(40), Constraint::Percentage(60)].as_ref())
108 | .split(body_chunks[1]);
109 |
110 | let help = draw_help(&app.actions);
111 | f.render_widget(help, sidebar[1]);
112 |
113 | f.render_widget(draw_selection(app), sidebar[0]);
114 |
115 | match app.show_input {
116 | true => {
117 | f.render_widget(draw_filter(app), chunks[1]);
118 | f.set_cursor(
119 | // Put cursor past the end of the input text
120 | chunks[1].x + app.input_cursor_position as u16 + 1,
121 | // Move one line down, from the border to the input line
122 | chunks[1].y + 1,
123 | )
124 | }
125 | false => f.render_widget(draw_logs(), chunks[1]),
126 | };
127 | }
128 |
129 | {
130 | let content_bar = Layout::default()
131 | .direction(Direction::Vertical)
132 | .constraints([Constraint::Length(3), Constraint::Min(20)].as_ref())
133 | .split(body_chunks[0]);
134 |
135 | f.render_widget(draw_sort(app), content_bar[0]);
136 |
137 | draw_table(app, f, content_bar[1]);
138 | }
139 |
140 | let p = { Paragraph::new(popup.popup_text.clone()) };
141 |
142 | if popup.visible {
143 | let rate_enabled = {
144 | let state = app.configuration.lock().unwrap();
145 | state.rate
146 | };
147 | let block = Block::default()
148 | .borders(Borders::ALL)
149 | .style(Style::default().bg(Color::Black));
150 | let p = p.block(block).alignment(Alignment::Center);
151 | let area = centered_rect(60, 20, area);
152 | f.render_widget(Clear, area);
153 | if exporting.load(std::sync::atomic::Ordering::Relaxed) && rate_enabled {
154 | while let Ok(pos) = percentage.try_recv() {
155 | debug!("exporting mirrors: progress {pos:.2}%");
156 | let gauge = Gauge::default()
157 | .gauge_style(Style::default().fg(Color::Blue).bg(Color::Black))
158 | .block(create_block("Exporting mirrors"))
159 | .percent(pos as u16);
160 | f.render_widget(gauge, area);
161 | }
162 | } else {
163 | f.render_widget(p, area);
164 | }
165 | }
166 | }
167 | }
168 |
169 | fn draw_table(app: &mut App, f: &mut Frame, region: Rect) {
170 | let header_cells = [" index", "╭─── country", "mirrors"]
171 | .iter()
172 | .map(|h| Cell::from(*h).style(Style::default()));
173 |
174 | if let Some(items) = app.mirrors.as_ref() {
175 | app.filtered_countries = items
176 | .countries
177 | .iter()
178 | .filter_map(|f| {
179 | let count = f.mirrors.iter().filter(|m| filter_result(app, m)).count();
180 | if count == 0 {
181 | None
182 | } else if f
183 | .name
184 | .to_ascii_lowercase()
185 | .contains(&app.input.to_ascii_lowercase())
186 | {
187 | Some((f.clone(), count))
188 | } else {
189 | None
190 | }
191 | })
192 | .sorted_by(|(f, count), (b, second_count)| {
193 | let config = app.configuration.lock().unwrap();
194 | match config.view {
195 | ViewSort::Alphabetical => Ord::cmp(&f.name, &b.name),
196 | ViewSort::MirrorCount => Ord::cmp(&second_count, &count),
197 | }
198 | })
199 | .collect_vec();
200 | };
201 |
202 | // 3 is the height offset
203 | app.table_viewport_height = region.height - 3;
204 |
205 | let rows = app.rows();
206 |
207 | let pagination_fragments = app.view_fragments(&rows);
208 |
209 | let header = Row::new(header_cells).height(1);
210 |
211 | let t = Table::new(
212 | if pagination_fragments.is_empty() {
213 | rows
214 | } else {
215 | app.view(&pagination_fragments).to_vec()
216 | },
217 | [
218 | Constraint::Percentage(6),
219 | Constraint::Length(33),
220 | Constraint::Min(10),
221 | ],
222 | )
223 | .header(header)
224 | .block(create_block(format!(
225 | "Results from ({}) countries",
226 | app.filtered_countries.len()
227 | )));
228 |
229 | f.render_widget(t, region);
230 | }
231 |
232 | fn draw_help(actions: &Actions) -> Table {
233 | let key_style = Style::default().fg(Color::LightCyan);
234 | let help_style = Style::default().fg(Color::Gray);
235 |
236 | let rows = actions.actions().iter().filter_map(|action| match action {
237 | Action::NavigateUp | Action::NavigateDown => None,
238 | _ => {
239 | let mut actions: Vec<_> = action
240 | .keys()
241 | .iter()
242 | .map(|k| Span::styled(k.to_string(), key_style))
243 | .collect();
244 |
245 | if actions.len() == 1 {
246 | actions.push(Span::raw(""));
247 | }
248 |
249 | let text = Span::styled(action.to_string(), help_style);
250 | actions.push(text);
251 | Some(Row::new(actions))
252 | }
253 | });
254 |
255 | Table::new(
256 | rows,
257 | [
258 | Constraint::Percentage(20),
259 | Constraint::Percentage(20),
260 | Constraint::Percentage(60),
261 | ],
262 | )
263 | .block(create_block("Help"))
264 | .column_spacing(1)
265 | }
266 |
267 | fn check_size(area: &Rect, width: u16, height: u16) -> bool {
268 | area.width < width || area.height < height
269 | }
270 |
271 | fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
272 | let popup_layout = Layout::default()
273 | .direction(Direction::Vertical)
274 | .constraints(
275 | [
276 | Constraint::Percentage((100 - percent_y) / 2),
277 | Constraint::Percentage(percent_y),
278 | Constraint::Percentage((100 - percent_y) / 2),
279 | ]
280 | .as_ref(),
281 | )
282 | .split(area);
283 |
284 | Layout::default()
285 | .direction(Direction::Horizontal)
286 | .constraints(
287 | [
288 | Constraint::Percentage((100 - percent_x) / 2),
289 | Constraint::Percentage(percent_x),
290 | Constraint::Percentage((100 - percent_x) / 2),
291 | ]
292 | .as_ref(),
293 | )
294 | .split(popup_layout[1])[1]
295 | }
296 |
297 | fn draw_logs<'a>() -> TuiLoggerWidget<'a> {
298 | TuiLoggerWidget::default()
299 | .style_error(Style::default().fg(Color::Red))
300 | .style_debug(Style::default().fg(Color::Blue))
301 | .style_warn(Style::default().fg(Color::Yellow))
302 | .style_trace(Style::default().fg(Color::Magenta))
303 | .style_info(Style::default().fg(Color::Green))
304 | .output_file(false)
305 | .output_timestamp(None)
306 | .output_line(false)
307 | .output_target(false)
308 | .block(create_block("Logs"))
309 | }
310 |
311 | fn draw_filter(app: &App) -> Paragraph {
312 | Paragraph::new(app.input.as_str()).block(create_block("Filter"))
313 | }
314 |
315 | fn draw_selection<'a>(app: &App) -> Table<'a> {
316 | let header_cells = ["code", "proto", "comp %", "delay", "dur", "score"]
317 | .iter()
318 | .map(|h| Cell::from(*h).style(Style::default()));
319 | let headers = Row::new(header_cells);
320 |
321 | let items = app.selected_mirrors.iter().map(|f| {
322 | let delay = f.delay.map(|f| {
323 | let duration = Duration::from_secs(f as u64);
324 | let minutes = (duration.as_secs() / 60) % 60;
325 | let hours = (duration.as_secs() / 60) / 60;
326 | (hours, minutes)
327 | });
328 |
329 | let score = f.score.map(format_float);
330 |
331 | let dur = f.duration_stddev.map(format_float);
332 |
333 | let completion = f.completion_pct;
334 |
335 | Row::new(vec![
336 | Cell::from(f.country_code.to_string()),
337 | Cell::from(f.protocol.to_string()),
338 | Cell::from(format!("{:.2}", (completion * 100.0))).style(if completion == 1.0 {
339 | Style::default().fg(Color::Green)
340 | } else if completion > 0.90 {
341 | Style::default().fg(Color::LightCyan)
342 | } else if completion > 0.80 {
343 | Style::default().fg(Color::Cyan)
344 | } else if completion > 0.70 {
345 | Style::default()
346 | .fg(Color::LightYellow)
347 | .add_modifier(Modifier::SLOW_BLINK)
348 | } else if completion > 0.60 {
349 | Style::default()
350 | .fg(Color::Yellow)
351 | .add_modifier(Modifier::SLOW_BLINK)
352 | } else if completion > 0.50 {
353 | Style::default()
354 | .fg(Color::LightRed)
355 | .add_modifier(Modifier::SLOW_BLINK)
356 | } else {
357 | Style::default()
358 | .fg(Color::Red)
359 | .add_modifier(Modifier::SLOW_BLINK)
360 | }),
361 | Cell::from(match delay {
362 | Some((hours, minutes)) => {
363 | format!("{hours}:{minutes}")
364 | }
365 | None => "-".to_string(),
366 | })
367 | .style(match delay {
368 | Some((hours, _)) => {
369 | if hours < 1 {
370 | Style::default().fg(Color::Green)
371 | } else {
372 | Style::default()
373 | }
374 | }
375 | None => Style::default(),
376 | }),
377 | Cell::from(
378 | dur.map(|f| f.to_string())
379 | .unwrap_or_else(|| "-".to_string()),
380 | ),
381 | Cell::from(
382 | score
383 | .map(|f| f.to_string())
384 | .unwrap_or_else(|| "-".to_string()),
385 | ),
386 | ])
387 | });
388 |
389 | let mirror_count = app.selected_mirrors.len();
390 | let config = app.configuration.lock().unwrap();
391 |
392 | let t = Table::new(
393 | items,
394 | [
395 | Constraint::Percentage(16),
396 | Constraint::Percentage(16),
397 | Constraint::Percentage(16),
398 | Constraint::Percentage(16),
399 | Constraint::Percentage(16),
400 | Constraint::Percentage(20),
401 | ],
402 | )
403 | // You can set the style of the entire Table.
404 | .style(Style::default().fg(Color::White))
405 | // It has an optional header, which is simply a Row always visible at the top.
406 | .header(headers)
407 | // As any other widget, a Table can be wrapped in a Block.
408 | .block(create_block(if mirror_count < 1 {
409 | format!("Selection({mirror_count})")
410 | } else {
411 | format!(
412 | "Selection({})▶ ({}) to {}",
413 | mirror_count,
414 | if config.export as usize <= mirror_count {
415 | config.export.to_string()
416 | } else {
417 | "ALL".to_string()
418 | },
419 | config.outfile.display()
420 | )
421 | }));
422 |
423 | t
424 | }
425 |
426 | fn draw_sort<'a>(app: &App) -> Paragraph<'a> {
427 | let config = app.configuration.lock().unwrap();
428 | let count: isize = config.filters.len() as isize;
429 | let active_sort = [config.view];
430 | let mut sorts: Vec<_> = active_sort
431 | .iter()
432 | .enumerate()
433 | .flat_map(|(idx, f)| {
434 | let mut ret = vec![
435 | Span::raw(format!(" [{f}]")),
436 | Span::styled(" ⇣", Style::default()),
437 | ];
438 | if (idx as isize) < count - 1 {
439 | ret.push(Span::styled(" 🢒", Style::default().fg(Color::Black)))
440 | }
441 | ret
442 | })
443 | .collect();
444 |
445 | let mut filters: Vec<_> = config
446 | .filters
447 | .iter()
448 | .enumerate()
449 | .flat_map(|(idx, f)| {
450 | let mut ret = vec![Span::styled(
451 | format!(" {f}"),
452 | Style::default()
453 | .fg(match f {
454 | Protocol::InSync => Color::Cyan,
455 | _ => Color::Blue,
456 | })
457 | .add_modifier(Modifier::BOLD),
458 | )];
459 | if (idx as isize) < count - 1 {
460 | ret.push(Span::styled(" 🢒", Style::default().fg(Color::Black)))
461 | }
462 | ret
463 | })
464 | .collect();
465 |
466 | sorts.append(&mut filters);
467 |
468 | let widget = Line::from(sorts);
469 |
470 | let bt = format!("Sort & Filter ({count})");
471 |
472 | Paragraph::new(widget).block(create_block(bt))
473 | }
474 |
475 | fn create_block<'a>(title: impl Into) -> Block<'a> {
476 | let title = title.into();
477 | Block::default()
478 | .borders(Borders::ALL)
479 | .border_type(BorderType::Rounded)
480 | .border_style(Style::default().fg(Color::Black))
481 | .title(Span::styled(
482 | format!(" {title} "),
483 | Style::default()
484 | .add_modifier(Modifier::BOLD)
485 | .fg(Color::White),
486 | ))
487 | }
488 |
489 | fn format_float(str: impl ToString) -> f32 {
490 | match str.to_string().parse::() {
491 | Ok(res) => (res * 100.0).round() / 100.0,
492 | Err(_) => -999.0,
493 | }
494 | }
495 |
496 | pub fn filter_result(app: &App, f: &Mirror) -> bool {
497 | use crate::config::Configuration;
498 | let mut config = app.configuration.lock().unwrap();
499 |
500 | let res = |config: &Configuration, f: &Mirror| {
501 | let mut completion_ok = config.completion_percent as f32 <= f.completion_pct * 100.0;
502 | let v4_on = config.filters.contains(&Protocol::Ipv4);
503 | let isos_on = config.filters.contains(&Protocol::Isos);
504 | let v6_on = config.filters.contains(&Protocol::Ipv6);
505 | if v4_on {
506 | completion_ok = completion_ok && f.ipv4;
507 | }
508 |
509 | if isos_on {
510 | completion_ok = completion_ok && f.isos;
511 | }
512 |
513 | if v6_on {
514 | completion_ok = completion_ok && f.ipv6;
515 | }
516 | completion_ok
517 | };
518 |
519 | if config.age != 0 {
520 | if let Some(mirror_sync) = f.last_sync {
521 | let now = Local::now();
522 | let mirror_sync: DateTime = DateTime::from(mirror_sync);
523 | let duration = now - mirror_sync;
524 | if !config.filters.contains(&Protocol::InSync) && app.show_insync {
525 | config.filters.push(Protocol::InSync);
526 | }
527 | duration.num_hours() <= config.age.into()
528 | && config.filters.contains(&Protocol::from(f.protocol))
529 | && res(&config, f)
530 | } else {
531 | false
532 | }
533 | } else {
534 | config.filters.contains(&Protocol::from(f.protocol)) && res(&config, f)
535 | }
536 | }
537 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/view/filter.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use crate::cli::Protocol;
4 |
5 | //#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, ValueEnum, Deserialize)]
6 | //#[serde(rename_all = "lowercase")]
7 | //pub enum Protocol {
8 | // Https,
9 | // Http,
10 | // Rsync,
11 | // #[value(skip)]
12 | // InSync,
13 | // #[value(skip)]
14 | // Ipv4,
15 | // #[value(skip)]
16 | // Ipv6,
17 | // #[value(skip)]
18 | // Isos,
19 | //}
20 |
21 | impl From for Protocol {
22 | fn from(value: archlinux::Protocol) -> Self {
23 | match value {
24 | archlinux::Protocol::Rsync => Self::Rsync,
25 | archlinux::Protocol::Http => Self::Http,
26 | archlinux::Protocol::Https => Self::Https,
27 | archlinux::Protocol::Ftp => Self::Ftp,
28 | }
29 | }
30 | }
31 |
32 | impl Display for Protocol {
33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 | write!(
35 | f,
36 | "{}",
37 | match self {
38 | Protocol::Https => "https",
39 | Protocol::Http => "http",
40 | Protocol::Rsync => "rsync",
41 | Protocol::Ftp => "ftp",
42 | Protocol::InSync => "in-sync",
43 | Protocol::Ipv4 => "ipv4",
44 | Protocol::Ipv6 => "ipv6",
45 | Protocol::Isos => "isos",
46 | }
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/view/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod filter;
2 | pub mod sort;
3 |
--------------------------------------------------------------------------------
/crates/mirro-rs/src/tui/view/sort.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use crate::cli::ViewSort;
4 |
5 | impl Display for ViewSort {
6 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7 | let str = match self {
8 | ViewSort::Alphabetical => "A",
9 | ViewSort::MirrorCount => "1",
10 | };
11 | write!(f, "{str}")
12 | }
13 | }
14 |
15 | #[allow(dead_code)]
16 | #[cfg_attr(test, derive(Default))]
17 | #[derive(PartialEq, Eq, Debug, Clone, Copy)]
18 | pub enum ExportSort {
19 | Completion,
20 | MirroringDelay,
21 | #[cfg_attr(test, default)]
22 | Duration,
23 | Score,
24 | }
25 |
26 | impl Display for ExportSort {
27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 | let str = match self {
29 | ExportSort::Completion => "%",
30 | ExportSort::MirroringDelay => "μ",
31 | ExportSort::Duration => "σ",
32 | ExportSort::Score => "~",
33 | };
34 | write!(f, "{str}")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/mirro-rs.json:
--------------------------------------------------------------------------------
1 | {
2 | "general": {
3 | "outfile": "/home/user/example/generated-mirrors",
4 | "export": 50,
5 | "view": "alphabetical",
6 | "sort": "score",
7 | "cache-ttl": 24,
8 | "url": "https://archlinux.org/mirrors/status/json/",
9 | "rate-speed": true,
10 | "timeout": 5
11 | },
12 | "filters": {
13 | "countries": [],
14 | "age": 24,
15 | "ipv6": true,
16 | "ipv4": true,
17 | "isos": true,
18 | "protocols": [
19 | "https",
20 | "http",
21 | "rsync"
22 | ],
23 | "completion-percent": 100
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/mirro-rs.toml:
--------------------------------------------------------------------------------
1 | [general]
2 | outfile = "/home/user/example/generated-mirrors" # must not end with trailing slash
3 | # Max number of mirrors to export
4 | export = 50
5 | view = "alphabetical" # alphabetical mirror-count
6 | sort = "score" # percentage, duration, delay, score
7 | cache-ttl = 24
8 | url = "https://archlinux.org/mirrors/status/json/"
9 | rate-speed = true
10 | timeout = 5
11 | #include = [
12 | # "https://cloudflaremirrors.com/archlinux/"
13 | #]
14 |
15 | [filters]
16 | countries = [ ]
17 | age = 0
18 | ipv6 = true
19 | ipv4 = true
20 | isos = true
21 | protocols = [ "https", "http", "rsync" ]
22 | completion-percent = 100
23 |
--------------------------------------------------------------------------------
/examples/mirro-rs.yaml:
--------------------------------------------------------------------------------
1 | general:
2 | outfile: /home/user/example/generated-mirrors
3 | export: 50
4 | view: alphabetical
5 | sort: score
6 | cache-ttl: 24
7 | url: https://archlinux.org/mirrors/status/json/
8 | rate-speed: true
9 | timeout: 5
10 | # include:
11 | # - https://cloudflaremirrors.com/archlinux/
12 | filters:
13 | countries: []
14 | age: 24
15 | ipv6: true
16 | ipv4: true
17 | isos: true
18 | protocols:
19 | - https
20 | - http
21 | - rsync
22 | completion-percent: 100
23 |
--------------------------------------------------------------------------------
/systemd/mirro-rs.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Update Pacman mirrorlists with mirro-rs
3 | Wants=network-online.target
4 | After=network-online.target
5 |
6 | [Service]
7 | Type=oneshot
8 | ExecStart=/usr/bin/mirro-rs -d -o /etc/pacman.d/mirrorlist --rate --protocols http --protocols https
9 |
10 | [Install]
11 | RequiredBy=multi-user.target
12 |
--------------------------------------------------------------------------------
/systemd/mirro-rs.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Refresh Pacman mirrorlist weekly with mirro-rs.
3 |
4 | [Timer]
5 | OnCalendar=weekly
6 | Persistent=true
7 | AccuracySec=1us
8 | RandomizedDelaySec=12h
9 |
10 | [Install]
11 | WantedBy=timers.target
12 |
--------------------------------------------------------------------------------