├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── errors.rs ├── main.rs ├── models ├── config.rs ├── leftwm.rs ├── mod.rs └── theme.rs ├── operations ├── apply.rs ├── autofind.rs ├── current.rs ├── install.rs ├── list.rs ├── migrate_toml_to_ron.rs ├── mod.rs ├── new.rs ├── search.rs ├── status.rs ├── support.rs ├── uninstall.rs ├── update.rs └── upgrade.rs └── utils ├── dir.rs ├── merge.rs ├── mod.rs ├── read.rs └── versions.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master, workflow-improvements ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility" 12 | RUSTDOCFLAGS: -Dwarnings 13 | RUSTUP_MAX_RETRIES: 10 14 | RUST_BACKTRACE: full 15 | CI: 1 16 | CARGO_NET_RETRY: 10 17 | CARGO_INCREMENTAL: 0 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: apt update 25 | run: sudo apt update 26 | - name: apt install libsystemd-dev 27 | run: sudo apt install -y --no-install-recommends libsystemd-dev 28 | - name: Install Rust 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: stable 33 | override: true 34 | - name: Cache Dependencies 35 | uses: Swatinem/rust-cache@ce325b60658c1b38465c06cc965b79baf32c1e72 36 | - name: Build 37 | run: cargo test --no-run --all-targets --all-features 38 | - name: Run tests 39 | run: cargo test --all-targets --all-features 40 | - name: Install Clippy 41 | run: rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu 42 | - name: Clippy 43 | run: cargo +stable clippy --all-targets --all-features 44 | 45 | fmt: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v2 49 | - name: Fmt 50 | run: cargo fmt -- --check 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload release artifacts 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | binaries: 10 | strategy: 11 | matrix: 12 | include: 13 | - os: ubuntu-latest 14 | rust_target: x86_64-unknown-linux-gnu 15 | asset_name: leftwm-theme_x86_64_linux_gnu 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | submodules: true 24 | 25 | - name: Cache 26 | uses: actions/cache@v3 27 | with: 28 | path: | 29 | ~/.cargo 30 | ~/.rustup 31 | target/ 32 | key: ${{ runner.os }}-${{ steps.rust-install.outputs.cachekey }}-${{ matrix.rust_target }}-binary-release 33 | 34 | - name: Install rust 35 | id: rust-install 36 | uses: dtolnay/rust-toolchain@stable 37 | with: 38 | targets: ${{ matrix.rust_target }} 39 | 40 | - name: Set Cargo.toml version 41 | if: github.event_name == 'release' 42 | shell: bash 43 | env: 44 | RELEASE_TAG: ${{ github.ref }} 45 | run: | 46 | mv Cargo.toml Cargo.toml.origl2 47 | sed "s/[0-9]*\\.[0-9]*\\.[0-9]*-git/${RELEASE_TAG##*\/v}/" Cargo.toml.origl2 > Cargo.toml 48 | mv Cargo.lock Cargo.lock.origl2 49 | sed "s/[0-9]*\\.[0-9]*\\.[0-9]*-git/${RELEASE_TAG##*\/v}/" Cargo.lock.origl2 > Cargo.lock 50 | - name: Install cross-compile linker for aarch64-unknown-linux-musl 51 | if: matrix.rust_target == 'aarch64-unknown-linux-musl' 52 | run: | 53 | sudo apt update 54 | sudo apt install gcc-aarch64-linux-gnu 55 | - name: Install openssl 56 | run: | 57 | sudo apt update 58 | sudo apt install openssl pkg-config libssl-dev 59 | cargo clean 60 | OPENSSL_LIB_DIR="/usr/lib/x86_64-linux-gnu" 61 | OPENSSL_INCLUDE_DIR="/usr/include/openssl" 62 | 63 | - name: Build 64 | env: 65 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: "/usr/bin/aarch64-linux-gnu-ld" 66 | run: | 67 | cargo +${{ steps.rust-install.outputs.name }} build --target ${{ matrix.rust_target }} --release --locked 68 | 69 | - name: Upload 70 | uses: svenstaro/upload-release-action@v2 71 | with: 72 | repo_token: ${{ secrets.GITHUB_TOKEN }} 73 | tag: ${{ github.ref }} 74 | file: target/${{ matrix.rust_target }}/release/leftwm-theme 75 | asset_name: ${{ matrix.asset_name }} 76 | 77 | crate: 78 | runs-on: ubuntu-latest 79 | 80 | steps: 81 | - name: Checkout 82 | uses: actions/checkout@v3 83 | with: 84 | submodules: true 85 | 86 | - name: Cache 87 | uses: actions/cache@v3 88 | with: 89 | path: | 90 | ~/.cargo 91 | ~/.rustup 92 | target/ 93 | key: ${{ runner.os }}-${{ steps.rust-install.outputs.cachekey }}-crate-release 94 | 95 | - name: Install rust 96 | id: rust-install 97 | uses: dtolnay/rust-toolchain@stable 98 | 99 | - name: Install openssl 100 | run: | 101 | sudo apt update 102 | sudo apt install openssl pkg-config libssl-dev 103 | cargo clean 104 | OPENSSL_LIB_DIR="/usr/lib/x86_64-linux-gnu" 105 | OPENSSL_INCLUDE_DIR="/usr/include/openssl" 106 | 107 | - name: Set Cargo.toml version 108 | if: github.event_name == 'release' 109 | shell: bash 110 | env: 111 | RELEASE_TAG: ${{ github.ref }} 112 | run: | 113 | mv Cargo.toml Cargo.toml.origl2 114 | sed "s/[0-9]*\\.[0-9]*\\.[0-9]*-git/${RELEASE_TAG##*\/v}/" Cargo.toml.origl2 > Cargo.toml 115 | mv Cargo.lock Cargo.lock.origl2 116 | sed "s/[0-9]*\\.[0-9]*\\.[0-9]*-git/${RELEASE_TAG##*\/v}/" Cargo.lock.origl2 > Cargo.lock 117 | - name: Publish crate 118 | if: github.event_name == 'release' 119 | env: 120 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 121 | run: | 122 | cargo publish --allow-dirty 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | /patches/ 6 | 7 | .* 8 | !/.gitignore 9 | 10 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 11 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 12 | #Cargo.lock 13 | 14 | # These are backup files generated by rustfmt 15 | **/*.rs.bk 16 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.13" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle" 45 | version = "1.0.6" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 48 | 49 | [[package]] 50 | name = "anstyle-parse" 51 | version = "0.2.3" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 54 | dependencies = [ 55 | "utf8parse", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-query" 60 | version = "1.0.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 63 | dependencies = [ 64 | "windows-sys 0.52.0", 65 | ] 66 | 67 | [[package]] 68 | name = "anstyle-wincon" 69 | version = "3.0.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 72 | dependencies = [ 73 | "anstyle", 74 | "windows-sys 0.52.0", 75 | ] 76 | 77 | [[package]] 78 | name = "autocfg" 79 | version = "1.1.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 82 | 83 | [[package]] 84 | name = "backtrace" 85 | version = "0.3.69" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 88 | dependencies = [ 89 | "addr2line", 90 | "cc", 91 | "cfg-if", 92 | "libc", 93 | "miniz_oxide", 94 | "object", 95 | "rustc-demangle", 96 | ] 97 | 98 | [[package]] 99 | name = "base64" 100 | version = "0.21.7" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 103 | 104 | [[package]] 105 | name = "bitflags" 106 | version = "1.3.2" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 109 | 110 | [[package]] 111 | name = "bitflags" 112 | version = "2.4.2" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 115 | dependencies = [ 116 | "serde", 117 | ] 118 | 119 | [[package]] 120 | name = "bumpalo" 121 | version = "3.15.3" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" 124 | 125 | [[package]] 126 | name = "bytes" 127 | version = "1.5.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 130 | 131 | [[package]] 132 | name = "cc" 133 | version = "1.0.89" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" 136 | dependencies = [ 137 | "jobserver", 138 | "libc", 139 | ] 140 | 141 | [[package]] 142 | name = "cfg-if" 143 | version = "1.0.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 146 | 147 | [[package]] 148 | name = "clap" 149 | version = "4.5.1" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" 152 | dependencies = [ 153 | "clap_builder", 154 | "clap_derive", 155 | ] 156 | 157 | [[package]] 158 | name = "clap_builder" 159 | version = "4.5.1" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" 162 | dependencies = [ 163 | "anstream", 164 | "anstyle", 165 | "clap_lex", 166 | "strsim", 167 | ] 168 | 169 | [[package]] 170 | name = "clap_derive" 171 | version = "4.5.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" 174 | dependencies = [ 175 | "heck", 176 | "proc-macro2", 177 | "quote", 178 | "syn", 179 | ] 180 | 181 | [[package]] 182 | name = "clap_lex" 183 | version = "0.7.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 186 | 187 | [[package]] 188 | name = "colorchoice" 189 | version = "1.0.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 192 | 193 | [[package]] 194 | name = "colored" 195 | version = "2.1.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 198 | dependencies = [ 199 | "lazy_static", 200 | "windows-sys 0.48.0", 201 | ] 202 | 203 | [[package]] 204 | name = "core-foundation" 205 | version = "0.9.4" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 208 | dependencies = [ 209 | "core-foundation-sys", 210 | "libc", 211 | ] 212 | 213 | [[package]] 214 | name = "core-foundation-sys" 215 | version = "0.8.6" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 218 | 219 | [[package]] 220 | name = "dirs-next" 221 | version = "2.0.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 224 | dependencies = [ 225 | "cfg-if", 226 | "dirs-sys-next", 227 | ] 228 | 229 | [[package]] 230 | name = "dirs-sys-next" 231 | version = "0.1.2" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 234 | dependencies = [ 235 | "libc", 236 | "redox_users", 237 | "winapi", 238 | ] 239 | 240 | [[package]] 241 | name = "edit-distance" 242 | version = "2.1.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b" 245 | 246 | [[package]] 247 | name = "encoding_rs" 248 | version = "0.8.33" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 251 | dependencies = [ 252 | "cfg-if", 253 | ] 254 | 255 | [[package]] 256 | name = "env_logger" 257 | version = "0.10.2" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 260 | dependencies = [ 261 | "humantime", 262 | "is-terminal", 263 | "log", 264 | "regex", 265 | "termcolor", 266 | ] 267 | 268 | [[package]] 269 | name = "equivalent" 270 | version = "1.0.1" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 273 | 274 | [[package]] 275 | name = "errno" 276 | version = "0.3.8" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 279 | dependencies = [ 280 | "libc", 281 | "windows-sys 0.52.0", 282 | ] 283 | 284 | [[package]] 285 | name = "fastrand" 286 | version = "2.0.1" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 289 | 290 | [[package]] 291 | name = "fnv" 292 | version = "1.0.7" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 295 | 296 | [[package]] 297 | name = "foreign-types" 298 | version = "0.3.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 301 | dependencies = [ 302 | "foreign-types-shared", 303 | ] 304 | 305 | [[package]] 306 | name = "foreign-types-shared" 307 | version = "0.1.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 310 | 311 | [[package]] 312 | name = "form_urlencoded" 313 | version = "1.2.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 316 | dependencies = [ 317 | "percent-encoding", 318 | ] 319 | 320 | [[package]] 321 | name = "futures" 322 | version = "0.3.30" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 325 | dependencies = [ 326 | "futures-channel", 327 | "futures-core", 328 | "futures-executor", 329 | "futures-io", 330 | "futures-sink", 331 | "futures-task", 332 | "futures-util", 333 | ] 334 | 335 | [[package]] 336 | name = "futures-channel" 337 | version = "0.3.30" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 340 | dependencies = [ 341 | "futures-core", 342 | "futures-sink", 343 | ] 344 | 345 | [[package]] 346 | name = "futures-core" 347 | version = "0.3.30" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 350 | 351 | [[package]] 352 | name = "futures-executor" 353 | version = "0.3.30" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 356 | dependencies = [ 357 | "futures-core", 358 | "futures-task", 359 | "futures-util", 360 | ] 361 | 362 | [[package]] 363 | name = "futures-io" 364 | version = "0.3.30" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 367 | 368 | [[package]] 369 | name = "futures-macro" 370 | version = "0.3.30" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 373 | dependencies = [ 374 | "proc-macro2", 375 | "quote", 376 | "syn", 377 | ] 378 | 379 | [[package]] 380 | name = "futures-sink" 381 | version = "0.3.30" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 384 | 385 | [[package]] 386 | name = "futures-task" 387 | version = "0.3.30" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 390 | 391 | [[package]] 392 | name = "futures-util" 393 | version = "0.3.30" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 396 | dependencies = [ 397 | "futures-channel", 398 | "futures-core", 399 | "futures-io", 400 | "futures-macro", 401 | "futures-sink", 402 | "futures-task", 403 | "memchr", 404 | "pin-project-lite", 405 | "pin-utils", 406 | "slab", 407 | ] 408 | 409 | [[package]] 410 | name = "fuzzy-matcher" 411 | version = "0.3.7" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 414 | dependencies = [ 415 | "thread_local", 416 | ] 417 | 418 | [[package]] 419 | name = "getrandom" 420 | version = "0.2.12" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 423 | dependencies = [ 424 | "cfg-if", 425 | "libc", 426 | "wasi", 427 | ] 428 | 429 | [[package]] 430 | name = "gimli" 431 | version = "0.28.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 434 | 435 | [[package]] 436 | name = "git2" 437 | version = "0.18.2" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" 440 | dependencies = [ 441 | "bitflags 2.4.2", 442 | "libc", 443 | "libgit2-sys", 444 | "log", 445 | "openssl-probe", 446 | "openssl-sys", 447 | "url", 448 | ] 449 | 450 | [[package]] 451 | name = "h2" 452 | version = "0.3.24" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" 455 | dependencies = [ 456 | "bytes", 457 | "fnv", 458 | "futures-core", 459 | "futures-sink", 460 | "futures-util", 461 | "http", 462 | "indexmap", 463 | "slab", 464 | "tokio", 465 | "tokio-util", 466 | "tracing", 467 | ] 468 | 469 | [[package]] 470 | name = "hashbrown" 471 | version = "0.14.3" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 474 | 475 | [[package]] 476 | name = "heck" 477 | version = "0.4.1" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 480 | 481 | [[package]] 482 | name = "hermit-abi" 483 | version = "0.3.9" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 486 | 487 | [[package]] 488 | name = "http" 489 | version = "0.2.12" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 492 | dependencies = [ 493 | "bytes", 494 | "fnv", 495 | "itoa", 496 | ] 497 | 498 | [[package]] 499 | name = "http-body" 500 | version = "0.4.6" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 503 | dependencies = [ 504 | "bytes", 505 | "http", 506 | "pin-project-lite", 507 | ] 508 | 509 | [[package]] 510 | name = "httparse" 511 | version = "1.8.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 514 | 515 | [[package]] 516 | name = "httpdate" 517 | version = "1.0.3" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 520 | 521 | [[package]] 522 | name = "humantime" 523 | version = "2.1.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 526 | 527 | [[package]] 528 | name = "hyper" 529 | version = "0.14.28" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" 532 | dependencies = [ 533 | "bytes", 534 | "futures-channel", 535 | "futures-core", 536 | "futures-util", 537 | "h2", 538 | "http", 539 | "http-body", 540 | "httparse", 541 | "httpdate", 542 | "itoa", 543 | "pin-project-lite", 544 | "socket2", 545 | "tokio", 546 | "tower-service", 547 | "tracing", 548 | "want", 549 | ] 550 | 551 | [[package]] 552 | name = "hyper-tls" 553 | version = "0.5.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 556 | dependencies = [ 557 | "bytes", 558 | "hyper", 559 | "native-tls", 560 | "tokio", 561 | "tokio-native-tls", 562 | ] 563 | 564 | [[package]] 565 | name = "idna" 566 | version = "0.5.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 569 | dependencies = [ 570 | "unicode-bidi", 571 | "unicode-normalization", 572 | ] 573 | 574 | [[package]] 575 | name = "indexmap" 576 | version = "2.2.5" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" 579 | dependencies = [ 580 | "equivalent", 581 | "hashbrown", 582 | ] 583 | 584 | [[package]] 585 | name = "ipnet" 586 | version = "2.9.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 589 | 590 | [[package]] 591 | name = "is-terminal" 592 | version = "0.4.12" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" 595 | dependencies = [ 596 | "hermit-abi", 597 | "libc", 598 | "windows-sys 0.52.0", 599 | ] 600 | 601 | [[package]] 602 | name = "itoa" 603 | version = "1.0.10" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 606 | 607 | [[package]] 608 | name = "jobserver" 609 | version = "0.1.28" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" 612 | dependencies = [ 613 | "libc", 614 | ] 615 | 616 | [[package]] 617 | name = "js-sys" 618 | version = "0.3.69" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 621 | dependencies = [ 622 | "wasm-bindgen", 623 | ] 624 | 625 | [[package]] 626 | name = "lazy_static" 627 | version = "1.4.0" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 630 | 631 | [[package]] 632 | name = "leftwm-core" 633 | version = "0.5.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "fececbfd34503c0b0cd65d5cc9b7babfa4cf2553c50d988f3ca1d07bb06a5fcf" 636 | dependencies = [ 637 | "dirs-next", 638 | "futures", 639 | "leftwm-layouts", 640 | "mio", 641 | "nix", 642 | "serde", 643 | "serde_json", 644 | "signal-hook", 645 | "thiserror", 646 | "tokio", 647 | "tracing", 648 | "x11-dl", 649 | "xdg", 650 | ] 651 | 652 | [[package]] 653 | name = "leftwm-layouts" 654 | version = "0.8.4" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "7ad24b8d3f59f0176ba4dc57a90955b16fd6e6c8a4fa384abf97f3861db6d073" 657 | dependencies = [ 658 | "serde", 659 | ] 660 | 661 | [[package]] 662 | name = "leftwm-theme" 663 | version = "0.1.3-git" 664 | dependencies = [ 665 | "clap", 666 | "colored", 667 | "dirs-next", 668 | "edit-distance", 669 | "fuzzy-matcher", 670 | "git2", 671 | "leftwm-core", 672 | "log", 673 | "openssl-sys", 674 | "pretty_env_logger", 675 | "regex", 676 | "reqwest", 677 | "ron", 678 | "semver", 679 | "serde", 680 | "serde_derive", 681 | "serde_json", 682 | "tempfile", 683 | "toml", 684 | "url", 685 | "xdg", 686 | ] 687 | 688 | [[package]] 689 | name = "libc" 690 | version = "0.2.153" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 693 | 694 | [[package]] 695 | name = "libgit2-sys" 696 | version = "0.16.2+1.7.2" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" 699 | dependencies = [ 700 | "cc", 701 | "libc", 702 | "libssh2-sys", 703 | "libz-sys", 704 | "openssl-sys", 705 | "pkg-config", 706 | ] 707 | 708 | [[package]] 709 | name = "libredox" 710 | version = "0.0.1" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 713 | dependencies = [ 714 | "bitflags 2.4.2", 715 | "libc", 716 | "redox_syscall", 717 | ] 718 | 719 | [[package]] 720 | name = "libssh2-sys" 721 | version = "0.3.0" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" 724 | dependencies = [ 725 | "cc", 726 | "libc", 727 | "libz-sys", 728 | "openssl-sys", 729 | "pkg-config", 730 | "vcpkg", 731 | ] 732 | 733 | [[package]] 734 | name = "libz-sys" 735 | version = "1.1.15" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" 738 | dependencies = [ 739 | "cc", 740 | "libc", 741 | "pkg-config", 742 | "vcpkg", 743 | ] 744 | 745 | [[package]] 746 | name = "linux-raw-sys" 747 | version = "0.4.13" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 750 | 751 | [[package]] 752 | name = "log" 753 | version = "0.4.21" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 756 | 757 | [[package]] 758 | name = "memchr" 759 | version = "2.7.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 762 | 763 | [[package]] 764 | name = "mime" 765 | version = "0.3.17" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 768 | 769 | [[package]] 770 | name = "miniz_oxide" 771 | version = "0.7.2" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 774 | dependencies = [ 775 | "adler", 776 | ] 777 | 778 | [[package]] 779 | name = "mio" 780 | version = "0.8.11" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 783 | dependencies = [ 784 | "libc", 785 | "log", 786 | "wasi", 787 | "windows-sys 0.48.0", 788 | ] 789 | 790 | [[package]] 791 | name = "native-tls" 792 | version = "0.2.11" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 795 | dependencies = [ 796 | "lazy_static", 797 | "libc", 798 | "log", 799 | "openssl", 800 | "openssl-probe", 801 | "openssl-sys", 802 | "schannel", 803 | "security-framework", 804 | "security-framework-sys", 805 | "tempfile", 806 | ] 807 | 808 | [[package]] 809 | name = "nix" 810 | version = "0.27.1" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 813 | dependencies = [ 814 | "bitflags 2.4.2", 815 | "cfg-if", 816 | "libc", 817 | ] 818 | 819 | [[package]] 820 | name = "num_cpus" 821 | version = "1.16.0" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 824 | dependencies = [ 825 | "hermit-abi", 826 | "libc", 827 | ] 828 | 829 | [[package]] 830 | name = "object" 831 | version = "0.32.2" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 834 | dependencies = [ 835 | "memchr", 836 | ] 837 | 838 | [[package]] 839 | name = "once_cell" 840 | version = "1.19.0" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 843 | 844 | [[package]] 845 | name = "openssl" 846 | version = "0.10.64" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" 849 | dependencies = [ 850 | "bitflags 2.4.2", 851 | "cfg-if", 852 | "foreign-types", 853 | "libc", 854 | "once_cell", 855 | "openssl-macros", 856 | "openssl-sys", 857 | ] 858 | 859 | [[package]] 860 | name = "openssl-macros" 861 | version = "0.1.1" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 864 | dependencies = [ 865 | "proc-macro2", 866 | "quote", 867 | "syn", 868 | ] 869 | 870 | [[package]] 871 | name = "openssl-probe" 872 | version = "0.1.5" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 875 | 876 | [[package]] 877 | name = "openssl-src" 878 | version = "300.2.3+3.2.1" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" 881 | dependencies = [ 882 | "cc", 883 | ] 884 | 885 | [[package]] 886 | name = "openssl-sys" 887 | version = "0.9.101" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" 890 | dependencies = [ 891 | "cc", 892 | "libc", 893 | "openssl-src", 894 | "pkg-config", 895 | "vcpkg", 896 | ] 897 | 898 | [[package]] 899 | name = "percent-encoding" 900 | version = "2.3.1" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 903 | 904 | [[package]] 905 | name = "pin-project-lite" 906 | version = "0.2.13" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 909 | 910 | [[package]] 911 | name = "pin-utils" 912 | version = "0.1.0" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 915 | 916 | [[package]] 917 | name = "pkg-config" 918 | version = "0.3.30" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 921 | 922 | [[package]] 923 | name = "pretty_env_logger" 924 | version = "0.5.0" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" 927 | dependencies = [ 928 | "env_logger", 929 | "log", 930 | ] 931 | 932 | [[package]] 933 | name = "proc-macro2" 934 | version = "1.0.78" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 937 | dependencies = [ 938 | "unicode-ident", 939 | ] 940 | 941 | [[package]] 942 | name = "quote" 943 | version = "1.0.35" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 946 | dependencies = [ 947 | "proc-macro2", 948 | ] 949 | 950 | [[package]] 951 | name = "redox_syscall" 952 | version = "0.4.1" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 955 | dependencies = [ 956 | "bitflags 1.3.2", 957 | ] 958 | 959 | [[package]] 960 | name = "redox_users" 961 | version = "0.4.4" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 964 | dependencies = [ 965 | "getrandom", 966 | "libredox", 967 | "thiserror", 968 | ] 969 | 970 | [[package]] 971 | name = "regex" 972 | version = "1.10.3" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 975 | dependencies = [ 976 | "aho-corasick", 977 | "memchr", 978 | "regex-automata", 979 | "regex-syntax", 980 | ] 981 | 982 | [[package]] 983 | name = "regex-automata" 984 | version = "0.4.6" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 987 | dependencies = [ 988 | "aho-corasick", 989 | "memchr", 990 | "regex-syntax", 991 | ] 992 | 993 | [[package]] 994 | name = "regex-syntax" 995 | version = "0.8.2" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 998 | 999 | [[package]] 1000 | name = "reqwest" 1001 | version = "0.11.24" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" 1004 | dependencies = [ 1005 | "base64", 1006 | "bytes", 1007 | "encoding_rs", 1008 | "futures-core", 1009 | "futures-util", 1010 | "h2", 1011 | "http", 1012 | "http-body", 1013 | "hyper", 1014 | "hyper-tls", 1015 | "ipnet", 1016 | "js-sys", 1017 | "log", 1018 | "mime", 1019 | "native-tls", 1020 | "once_cell", 1021 | "percent-encoding", 1022 | "pin-project-lite", 1023 | "rustls-pemfile", 1024 | "serde", 1025 | "serde_json", 1026 | "serde_urlencoded", 1027 | "sync_wrapper", 1028 | "system-configuration", 1029 | "tokio", 1030 | "tokio-native-tls", 1031 | "tower-service", 1032 | "url", 1033 | "wasm-bindgen", 1034 | "wasm-bindgen-futures", 1035 | "web-sys", 1036 | "winreg", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "ron" 1041 | version = "0.8.1" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" 1044 | dependencies = [ 1045 | "base64", 1046 | "bitflags 2.4.2", 1047 | "serde", 1048 | "serde_derive", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "rustc-demangle" 1053 | version = "0.1.23" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 1056 | 1057 | [[package]] 1058 | name = "rustix" 1059 | version = "0.38.31" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 1062 | dependencies = [ 1063 | "bitflags 2.4.2", 1064 | "errno", 1065 | "libc", 1066 | "linux-raw-sys", 1067 | "windows-sys 0.52.0", 1068 | ] 1069 | 1070 | [[package]] 1071 | name = "rustls-pemfile" 1072 | version = "1.0.4" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1075 | dependencies = [ 1076 | "base64", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "ryu" 1081 | version = "1.0.17" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 1084 | 1085 | [[package]] 1086 | name = "schannel" 1087 | version = "0.1.23" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 1090 | dependencies = [ 1091 | "windows-sys 0.52.0", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "security-framework" 1096 | version = "2.9.2" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" 1099 | dependencies = [ 1100 | "bitflags 1.3.2", 1101 | "core-foundation", 1102 | "core-foundation-sys", 1103 | "libc", 1104 | "security-framework-sys", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "security-framework-sys" 1109 | version = "2.9.1" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" 1112 | dependencies = [ 1113 | "core-foundation-sys", 1114 | "libc", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "semver" 1119 | version = "1.0.22" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 1122 | 1123 | [[package]] 1124 | name = "serde" 1125 | version = "1.0.197" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 1128 | dependencies = [ 1129 | "serde_derive", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "serde_derive" 1134 | version = "1.0.197" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 1137 | dependencies = [ 1138 | "proc-macro2", 1139 | "quote", 1140 | "syn", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "serde_json" 1145 | version = "1.0.114" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 1148 | dependencies = [ 1149 | "itoa", 1150 | "ryu", 1151 | "serde", 1152 | ] 1153 | 1154 | [[package]] 1155 | name = "serde_spanned" 1156 | version = "0.6.5" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 1159 | dependencies = [ 1160 | "serde", 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "serde_urlencoded" 1165 | version = "0.7.1" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1168 | dependencies = [ 1169 | "form_urlencoded", 1170 | "itoa", 1171 | "ryu", 1172 | "serde", 1173 | ] 1174 | 1175 | [[package]] 1176 | name = "signal-hook" 1177 | version = "0.3.17" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1180 | dependencies = [ 1181 | "libc", 1182 | "signal-hook-registry", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "signal-hook-registry" 1187 | version = "1.4.1" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 1190 | dependencies = [ 1191 | "libc", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "slab" 1196 | version = "0.4.9" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1199 | dependencies = [ 1200 | "autocfg", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "socket2" 1205 | version = "0.5.6" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 1208 | dependencies = [ 1209 | "libc", 1210 | "windows-sys 0.52.0", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "strsim" 1215 | version = "0.11.0" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 1218 | 1219 | [[package]] 1220 | name = "syn" 1221 | version = "2.0.52" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" 1224 | dependencies = [ 1225 | "proc-macro2", 1226 | "quote", 1227 | "unicode-ident", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "sync_wrapper" 1232 | version = "0.1.2" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1235 | 1236 | [[package]] 1237 | name = "system-configuration" 1238 | version = "0.5.1" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1241 | dependencies = [ 1242 | "bitflags 1.3.2", 1243 | "core-foundation", 1244 | "system-configuration-sys", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "system-configuration-sys" 1249 | version = "0.5.0" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1252 | dependencies = [ 1253 | "core-foundation-sys", 1254 | "libc", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "tempfile" 1259 | version = "3.10.1" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 1262 | dependencies = [ 1263 | "cfg-if", 1264 | "fastrand", 1265 | "rustix", 1266 | "windows-sys 0.52.0", 1267 | ] 1268 | 1269 | [[package]] 1270 | name = "termcolor" 1271 | version = "1.4.1" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1274 | dependencies = [ 1275 | "winapi-util", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "thiserror" 1280 | version = "1.0.57" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 1283 | dependencies = [ 1284 | "thiserror-impl", 1285 | ] 1286 | 1287 | [[package]] 1288 | name = "thiserror-impl" 1289 | version = "1.0.57" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 1292 | dependencies = [ 1293 | "proc-macro2", 1294 | "quote", 1295 | "syn", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "thread_local" 1300 | version = "1.1.8" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1303 | dependencies = [ 1304 | "cfg-if", 1305 | "once_cell", 1306 | ] 1307 | 1308 | [[package]] 1309 | name = "tinyvec" 1310 | version = "1.6.0" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1313 | dependencies = [ 1314 | "tinyvec_macros", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "tinyvec_macros" 1319 | version = "0.1.1" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1322 | 1323 | [[package]] 1324 | name = "tokio" 1325 | version = "1.36.0" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" 1328 | dependencies = [ 1329 | "backtrace", 1330 | "bytes", 1331 | "libc", 1332 | "mio", 1333 | "num_cpus", 1334 | "pin-project-lite", 1335 | "socket2", 1336 | "tokio-macros", 1337 | "windows-sys 0.48.0", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "tokio-macros" 1342 | version = "2.2.0" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 1345 | dependencies = [ 1346 | "proc-macro2", 1347 | "quote", 1348 | "syn", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "tokio-native-tls" 1353 | version = "0.3.1" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1356 | dependencies = [ 1357 | "native-tls", 1358 | "tokio", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "tokio-util" 1363 | version = "0.7.10" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 1366 | dependencies = [ 1367 | "bytes", 1368 | "futures-core", 1369 | "futures-sink", 1370 | "pin-project-lite", 1371 | "tokio", 1372 | "tracing", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "toml" 1377 | version = "0.8.10" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" 1380 | dependencies = [ 1381 | "serde", 1382 | "serde_spanned", 1383 | "toml_datetime", 1384 | "toml_edit", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "toml_datetime" 1389 | version = "0.6.5" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 1392 | dependencies = [ 1393 | "serde", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "toml_edit" 1398 | version = "0.22.6" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" 1401 | dependencies = [ 1402 | "indexmap", 1403 | "serde", 1404 | "serde_spanned", 1405 | "toml_datetime", 1406 | "winnow", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "tower-service" 1411 | version = "0.3.2" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1414 | 1415 | [[package]] 1416 | name = "tracing" 1417 | version = "0.1.40" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1420 | dependencies = [ 1421 | "pin-project-lite", 1422 | "tracing-attributes", 1423 | "tracing-core", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "tracing-attributes" 1428 | version = "0.1.27" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1431 | dependencies = [ 1432 | "proc-macro2", 1433 | "quote", 1434 | "syn", 1435 | ] 1436 | 1437 | [[package]] 1438 | name = "tracing-core" 1439 | version = "0.1.32" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1442 | dependencies = [ 1443 | "once_cell", 1444 | ] 1445 | 1446 | [[package]] 1447 | name = "try-lock" 1448 | version = "0.2.5" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1451 | 1452 | [[package]] 1453 | name = "unicode-bidi" 1454 | version = "0.3.15" 1455 | source = "registry+https://github.com/rust-lang/crates.io-index" 1456 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1457 | 1458 | [[package]] 1459 | name = "unicode-ident" 1460 | version = "1.0.12" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1463 | 1464 | [[package]] 1465 | name = "unicode-normalization" 1466 | version = "0.1.23" 1467 | source = "registry+https://github.com/rust-lang/crates.io-index" 1468 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1469 | dependencies = [ 1470 | "tinyvec", 1471 | ] 1472 | 1473 | [[package]] 1474 | name = "url" 1475 | version = "2.5.0" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1478 | dependencies = [ 1479 | "form_urlencoded", 1480 | "idna", 1481 | "percent-encoding", 1482 | ] 1483 | 1484 | [[package]] 1485 | name = "utf8parse" 1486 | version = "0.2.1" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1489 | 1490 | [[package]] 1491 | name = "vcpkg" 1492 | version = "0.2.15" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1495 | 1496 | [[package]] 1497 | name = "want" 1498 | version = "0.3.1" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1501 | dependencies = [ 1502 | "try-lock", 1503 | ] 1504 | 1505 | [[package]] 1506 | name = "wasi" 1507 | version = "0.11.0+wasi-snapshot-preview1" 1508 | source = "registry+https://github.com/rust-lang/crates.io-index" 1509 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1510 | 1511 | [[package]] 1512 | name = "wasm-bindgen" 1513 | version = "0.2.92" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1516 | dependencies = [ 1517 | "cfg-if", 1518 | "wasm-bindgen-macro", 1519 | ] 1520 | 1521 | [[package]] 1522 | name = "wasm-bindgen-backend" 1523 | version = "0.2.92" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1526 | dependencies = [ 1527 | "bumpalo", 1528 | "log", 1529 | "once_cell", 1530 | "proc-macro2", 1531 | "quote", 1532 | "syn", 1533 | "wasm-bindgen-shared", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "wasm-bindgen-futures" 1538 | version = "0.4.42" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 1541 | dependencies = [ 1542 | "cfg-if", 1543 | "js-sys", 1544 | "wasm-bindgen", 1545 | "web-sys", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "wasm-bindgen-macro" 1550 | version = "0.2.92" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1553 | dependencies = [ 1554 | "quote", 1555 | "wasm-bindgen-macro-support", 1556 | ] 1557 | 1558 | [[package]] 1559 | name = "wasm-bindgen-macro-support" 1560 | version = "0.2.92" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1563 | dependencies = [ 1564 | "proc-macro2", 1565 | "quote", 1566 | "syn", 1567 | "wasm-bindgen-backend", 1568 | "wasm-bindgen-shared", 1569 | ] 1570 | 1571 | [[package]] 1572 | name = "wasm-bindgen-shared" 1573 | version = "0.2.92" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1576 | 1577 | [[package]] 1578 | name = "web-sys" 1579 | version = "0.3.69" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 1582 | dependencies = [ 1583 | "js-sys", 1584 | "wasm-bindgen", 1585 | ] 1586 | 1587 | [[package]] 1588 | name = "winapi" 1589 | version = "0.3.9" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1592 | dependencies = [ 1593 | "winapi-i686-pc-windows-gnu", 1594 | "winapi-x86_64-pc-windows-gnu", 1595 | ] 1596 | 1597 | [[package]] 1598 | name = "winapi-i686-pc-windows-gnu" 1599 | version = "0.4.0" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1602 | 1603 | [[package]] 1604 | name = "winapi-util" 1605 | version = "0.1.6" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1608 | dependencies = [ 1609 | "winapi", 1610 | ] 1611 | 1612 | [[package]] 1613 | name = "winapi-x86_64-pc-windows-gnu" 1614 | version = "0.4.0" 1615 | source = "registry+https://github.com/rust-lang/crates.io-index" 1616 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1617 | 1618 | [[package]] 1619 | name = "windows-sys" 1620 | version = "0.48.0" 1621 | source = "registry+https://github.com/rust-lang/crates.io-index" 1622 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1623 | dependencies = [ 1624 | "windows-targets 0.48.5", 1625 | ] 1626 | 1627 | [[package]] 1628 | name = "windows-sys" 1629 | version = "0.52.0" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1632 | dependencies = [ 1633 | "windows-targets 0.52.4", 1634 | ] 1635 | 1636 | [[package]] 1637 | name = "windows-targets" 1638 | version = "0.48.5" 1639 | source = "registry+https://github.com/rust-lang/crates.io-index" 1640 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1641 | dependencies = [ 1642 | "windows_aarch64_gnullvm 0.48.5", 1643 | "windows_aarch64_msvc 0.48.5", 1644 | "windows_i686_gnu 0.48.5", 1645 | "windows_i686_msvc 0.48.5", 1646 | "windows_x86_64_gnu 0.48.5", 1647 | "windows_x86_64_gnullvm 0.48.5", 1648 | "windows_x86_64_msvc 0.48.5", 1649 | ] 1650 | 1651 | [[package]] 1652 | name = "windows-targets" 1653 | version = "0.52.4" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 1656 | dependencies = [ 1657 | "windows_aarch64_gnullvm 0.52.4", 1658 | "windows_aarch64_msvc 0.52.4", 1659 | "windows_i686_gnu 0.52.4", 1660 | "windows_i686_msvc 0.52.4", 1661 | "windows_x86_64_gnu 0.52.4", 1662 | "windows_x86_64_gnullvm 0.52.4", 1663 | "windows_x86_64_msvc 0.52.4", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "windows_aarch64_gnullvm" 1668 | version = "0.48.5" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1671 | 1672 | [[package]] 1673 | name = "windows_aarch64_gnullvm" 1674 | version = "0.52.4" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 1677 | 1678 | [[package]] 1679 | name = "windows_aarch64_msvc" 1680 | version = "0.48.5" 1681 | source = "registry+https://github.com/rust-lang/crates.io-index" 1682 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1683 | 1684 | [[package]] 1685 | name = "windows_aarch64_msvc" 1686 | version = "0.52.4" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 1689 | 1690 | [[package]] 1691 | name = "windows_i686_gnu" 1692 | version = "0.48.5" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1695 | 1696 | [[package]] 1697 | name = "windows_i686_gnu" 1698 | version = "0.52.4" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 1701 | 1702 | [[package]] 1703 | name = "windows_i686_msvc" 1704 | version = "0.48.5" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1707 | 1708 | [[package]] 1709 | name = "windows_i686_msvc" 1710 | version = "0.52.4" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 1713 | 1714 | [[package]] 1715 | name = "windows_x86_64_gnu" 1716 | version = "0.48.5" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1719 | 1720 | [[package]] 1721 | name = "windows_x86_64_gnu" 1722 | version = "0.52.4" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 1725 | 1726 | [[package]] 1727 | name = "windows_x86_64_gnullvm" 1728 | version = "0.48.5" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1731 | 1732 | [[package]] 1733 | name = "windows_x86_64_gnullvm" 1734 | version = "0.52.4" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 1737 | 1738 | [[package]] 1739 | name = "windows_x86_64_msvc" 1740 | version = "0.48.5" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1743 | 1744 | [[package]] 1745 | name = "windows_x86_64_msvc" 1746 | version = "0.52.4" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 1749 | 1750 | [[package]] 1751 | name = "winnow" 1752 | version = "0.6.5" 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" 1754 | checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" 1755 | dependencies = [ 1756 | "memchr", 1757 | ] 1758 | 1759 | [[package]] 1760 | name = "winreg" 1761 | version = "0.50.0" 1762 | source = "registry+https://github.com/rust-lang/crates.io-index" 1763 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1764 | dependencies = [ 1765 | "cfg-if", 1766 | "windows-sys 0.48.0", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "x11-dl" 1771 | version = "2.21.0" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" 1774 | dependencies = [ 1775 | "libc", 1776 | "once_cell", 1777 | "pkg-config", 1778 | ] 1779 | 1780 | [[package]] 1781 | name = "xdg" 1782 | version = "2.5.2" 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" 1784 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 1785 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leftwm-theme" 3 | version = "0.1.3-git" 4 | authors = ["Lex Childs ", "Hitesh Paul ", "Mautamu "] 5 | description = "A theme mangager for LeftWM" 6 | edition = "2018" 7 | keywords = ["leftwm", "wm", "theme"] 8 | license = "MIT" 9 | repository = "https://github.com/leftwm/leftwm-theme" 10 | readme = "README.md" 11 | resolver="1" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | clap = { version = "4.4", features=["derive"] } 17 | colored = "2.0.0" 18 | dirs-next = "2.0.0" 19 | edit-distance = "2.1.0" 20 | fuzzy-matcher = "0.3.7" 21 | git2 = {version = "0.18", features=["vendored-libgit2"]} 22 | leftwm-core = "0.5" 23 | log = "0.4" 24 | pretty_env_logger = "0.5.0" 25 | regex = "1.5.4" 26 | reqwest = {version = "0.11.0", features= ["blocking", "json"]} 27 | ron = "0.8.0" 28 | semver = "1.0.1" 29 | serde = "1.0.104" 30 | serde_derive = "1.0.104" 31 | serde_json = "1.0.44" 32 | tempfile = "3.2.0" 33 | toml = "0.8" 34 | xdg = "2.2.0" 35 | url = "2.2.2" 36 | 37 | 38 | [dependencies.openssl-sys] 39 | version = "0.9" 40 | features = ["vendored"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, leftwm 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

leftwm-theme

3 | 4 |

5 | A theme manager for 6 | LeftWM 7 |

8 |

9 | build status 10 | 11 | 12 |

13 |
14 | 15 | 16 | 17 | ## Installation 18 | Currently, LeftWM-theme is only available by Git and AUR. 19 | 20 | To install by AUR (Arch-based distributions only), use package `leftwm-theme-git`. 21 | 22 | To install manually, clone this repository: 23 | ```bash 24 | git clone https://github.com/leftwm/leftwm-theme 25 | ``` 26 | Then enter the repository: 27 | ``` 28 | cd leftwm-theme 29 | ``` 30 | Then build with Cargo: 31 | ```bash 32 | cargo build --release 33 | ``` 34 | You can then install LeftWM-theme: 35 | ```bash 36 | # for production installations (does not update when recompiled) 37 | sudo install -s -Dm755 ./target/release/leftwm-theme -t /usr/bin 38 | #-- or -- 39 | # for developer installations (updates when recompiled) 40 | sudo ln -s "$(pwd)"/target/release/leftwm-theme /usr/bin/leftwm-theme 41 | ``` 42 | 43 | ## Usage 44 | ### First use 45 | The first time you start up LeftWM-theme, it will generate a file called `themes.toml` in your `~/.config/leftwm/` folder from the themes located in the [Community Themes](https://github.com/leftwm/leftwm-community-themes) repo. To do so, run: 46 | ```bash 47 | leftwm-theme update 48 | ``` 49 | 56 | ### Install a theme 57 | LeftWM-theme differentiates between _installing_ a theme and _applying_ a theme. Installing a theme is akin to downloading it; behind the scenes LeftWM-theme runs `git clone {theme}`. No dependency checks are performed at installation time, but instead at application time. To install a theme, for example the fabulous Orange Forest theme, run (quotation marks needed for names with spaces): 58 | ```bash 59 | leftwm-theme install "Orange Forest" 60 | ``` 61 | **Note: LeftWM-theme is CaSe SeNsItIvE, so be careful!** 62 | 63 | ### Apply a theme 64 | LeftWM-theme will check for dependencies, LeftWM-version, and the like during the application process. 65 | Now that you've installed Orange Forest (or whatever theme you like), to set it as your current theme, run: 66 | ```bash 67 | leftwm-theme apply "Orange Forest" 68 | ``` 69 | **Note: LeftWM should automatically restart with the new theme** 70 | 71 | ### List installed themes 72 | To list all installed themes that LeftWM-theme knows about, run: 73 | ```bash 74 | leftwm-theme list 75 | ``` 76 | 77 | ### See current theme 78 | Although multiple commands list the installed theme, using the following can provide additional context: 79 | ```bash 80 | leftwm-theme status 81 | ``` 82 | 83 | ### Update theme list 84 | To update your copy of the themes, use the following: 85 | ```bash 86 | leftwm-theme update 87 | ``` 88 | **Note: this does not also update the themes, just the repository listings! To update themes see upgrade** 89 | 90 | ### Updating themes 91 | To update themes, use the following: 92 | ```bash 93 | leftwm-theme upgrade 94 | ``` 95 | **Note: this command also updates repositories** 96 | 97 | ### Adding a repository 98 | Leftwm-theme allows multiple `known.toml` repositories to be used. To add another repository, it must have a `known.toml` file which you can add to `themes.toml` in your LeftWM config folder. 99 | 100 | **Note: It is wise to backup your `themes.toml` file PRIOR to adding a new repostitory** 101 | 102 | To add a repository, add the following to the BOTTOM of your `themes.toml` file, located at `~/.config/leftwm/themes.toml`: 103 | ```toml 104 | # To add additional repos, you MUST specify a url, a UNIQUE ALPHANUMERIC name, and an empty array of themes 105 | # e.g.: 106 | # [[repos]] 107 | # url = "https://raw.githubusercontant.com/mautamu/leftwm-community-themes/master/known.toml" 108 | # name = "mautamu" 109 | # themes = [] 110 | [[repos]] 111 | url = "" 112 | name = "" 113 | themes = [] 114 | ``` 115 | **Note: be sure that the url points to a file called known.toml, such as https://raw.githubusercontent.com/leftwm/leftwm-community-themes/master/known.toml** 116 | 117 | Then fill in the url with the url of that repo and add a descriptive name that consists of only letters and numbers [A-z0-9]. To load themes from the repository, use the following: 118 | ```bash 119 | leftwm-theme -vvv update 120 | ``` 121 | **Note: the -vvv flag is not necessary, but will provide additional output in case your new repo goes wrong** 122 | ## Troubleshooting 123 | ### Themes.toml is nearly empty, and/or LeftWM won't update my themes: 124 | Try removing themes.toml and running the `update` command, add any repositories that were removed, and then run `autofind` to repopulate your installed themes. 125 | ### I can't get a theme to install 126 | Double check your name. Although `update` may say `mautam/theme`, you just need to type `theme`, not `mautam/theme`. Pay attention to capital letters and spelling. 127 | 128 | 129 | ## Roadmap: 130 | ### Version 0.1.0 131 | - [x] Allow users to install themes 132 | - [x] Allow users to remove themes 133 | - [x] Allow a theme to be applied as current 134 | - [x] Check dependencies for a theme 135 | - [x] Allow dependency override with -n 136 | - [ ] Offer suggestions for dependency installation 137 | - [ ] Check whether a theme's `theme.toml` file is valid 138 | - [x] Allow themes to specify compatible LeftWM versions 139 | - [ ] Find themes located in ~/.config/leftwm/themes/ automatically 140 | - [x] Allow users to add more theme repositories 141 | - [x] Allow users to choose from which repository to install themes 142 | - [x] Allow users to create new themes 143 | - [x] Provide basic themes for users to fork into their own 144 | - [ ] Generate appropriate `known.toml` pull requests and `theme.toml` files 145 | - [ ] Make sure themes don't include `/` or other OS-specific marks. **Partially complete** 146 | - [x] Allow users to update their repository theme lists with `update` as in apt-get form 147 | - [x] Allow users to update their themes with `upgrade` command, as in apt-get form 148 | - [x] Allow users to skip repo update 149 | - [ ] Perform dependency checks prior to updating the current theme 150 | - [x] Allow users to search for themes by name 151 | ### Version 0.2.0 152 | - [ ] Extend `theme.toml` to allow for up/down specifications within `theme.toml` 153 | - [ ] Integrate `themes.toml` and `known.toml` better 154 | - [ ] Reduce the number of dependencies 155 | - [ ] Replace Reqwest with a crate with fewer dependencies 156 | - [ ] Examine other areas of overlapping features 157 | - [ ] Provision for name aliases for dependencies in different distros 158 | - [ ] Improve documentation 159 | - [ ] Better, more consistent error handling 160 | - [x] Remove `nightly` Rust requirement by replacing `?` on Options 161 | - [ ] Add a testing suite 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use url::ParseError as urlParseError; 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Debug)] 6 | pub struct LeftError { 7 | pub inner: LeftErrorKind, 8 | } 9 | 10 | #[must_use] 11 | pub fn friendly_message(msg: &str) -> LeftError { 12 | LeftError { 13 | inner: LeftErrorKind::UserFriendlyError(msg.to_string()), 14 | } 15 | } 16 | 17 | #[derive(Debug)] 18 | pub enum LeftErrorKind { 19 | SerdeParse(serde_json::error::Error), 20 | IoError(std::io::Error), 21 | XdgBaseDirError(xdg::BaseDirectoriesError), 22 | TomlParse(toml::de::Error), 23 | TomlSerialize(toml::ser::Error), 24 | ReqwestError(reqwest::Error), 25 | StreamError(), 26 | NoneError(), 27 | UserFriendlyError(String), 28 | GitError(git2::Error), 29 | Generic(String), 30 | ParseIntError(core::num::ParseIntError), 31 | SemVerError(semver::Error), 32 | UrlParseError(url::ParseError), 33 | RonError(ron::Error), 34 | } 35 | 36 | impl fmt::Display for LeftError { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | write!(f, "{}", self.inner) 39 | } 40 | } 41 | 42 | impl fmt::Display for LeftErrorKind { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | match self { 45 | LeftErrorKind::SerdeParse(ref err) => write!(f, "{err}"), 46 | LeftErrorKind::UserFriendlyError(ref err) | LeftErrorKind::Generic(ref err) => { 47 | write!(f, "{err}") 48 | } 49 | LeftErrorKind::IoError(ref err) => write!(f, "{err}"), 50 | LeftErrorKind::XdgBaseDirError(ref err) => write!(f, "{err}"), 51 | LeftErrorKind::TomlParse(ref err) => write!(f, "{err}"), 52 | LeftErrorKind::TomlSerialize(ref err) => write!(f, "{err}"), 53 | LeftErrorKind::StreamError() => write!(f, "Stream Error"), 54 | LeftErrorKind::NoneError() => write!(f, "None Error"), 55 | LeftErrorKind::ReqwestError(ref err) => write!(f, "Request Error: {err}"), 56 | LeftErrorKind::GitError(ref err) => write!(f, "{err}"), 57 | LeftErrorKind::ParseIntError(ref err) => write!(f, "{err}"), 58 | LeftErrorKind::SemVerError(ref err) => write!(f, "{err}"), 59 | LeftErrorKind::UrlParseError(ref err) => write!(f, "{err}"), 60 | LeftErrorKind::RonError(ref err) => write!(f, "{err}"), 61 | } 62 | } 63 | } 64 | 65 | impl From for LeftError { 66 | fn from(inner: LeftErrorKind) -> LeftError { 67 | LeftError { inner } 68 | } 69 | } 70 | 71 | impl From for LeftError { 72 | fn from(inner: serde_json::error::Error) -> LeftError { 73 | LeftErrorKind::SerdeParse(inner).into() 74 | } 75 | } 76 | 77 | impl From for LeftError { 78 | fn from(inner: std::io::Error) -> LeftError { 79 | LeftErrorKind::IoError(inner).into() 80 | } 81 | } 82 | 83 | impl From for LeftError { 84 | fn from(inner: xdg::BaseDirectoriesError) -> LeftError { 85 | LeftErrorKind::XdgBaseDirError(inner).into() 86 | } 87 | } 88 | 89 | impl From for LeftError { 90 | fn from(inner: toml::de::Error) -> LeftError { 91 | LeftErrorKind::TomlParse(inner).into() 92 | } 93 | } 94 | 95 | impl From for LeftError { 96 | fn from(inner: toml::ser::Error) -> LeftError { 97 | LeftErrorKind::TomlSerialize(inner).into() 98 | } 99 | } 100 | 101 | impl From for LeftError { 102 | fn from(inner: reqwest::Error) -> LeftError { 103 | LeftErrorKind::ReqwestError(inner).into() 104 | } 105 | } 106 | 107 | impl From<&str> for LeftError { 108 | fn from(_s: &str) -> LeftError { 109 | LeftErrorKind::NoneError().into() 110 | } 111 | } 112 | 113 | impl From for LeftError { 114 | fn from(inner: git2::Error) -> LeftError { 115 | LeftErrorKind::GitError(inner).into() 116 | } 117 | } 118 | 119 | impl From for LeftError { 120 | fn from(inner: core::num::ParseIntError) -> LeftError { 121 | LeftErrorKind::ParseIntError(inner).into() 122 | } 123 | } 124 | 125 | impl From for LeftError { 126 | fn from(inner: semver::Error) -> LeftError { 127 | LeftErrorKind::SemVerError(inner).into() 128 | } 129 | } 130 | 131 | impl From for LeftError { 132 | fn from(inner: urlParseError) -> LeftError { 133 | LeftErrorKind::UrlParseError(inner).into() 134 | } 135 | } 136 | 137 | impl From for LeftError { 138 | fn from(inner: ron::Error) -> LeftError { 139 | LeftErrorKind::RonError(inner).into() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Various leftwm features. 2 | // We deny clippy pedantic lints, primarily to keep code as correct as possible 3 | // Remember, the goal of LeftWM is to do one thing and to do that one thing 4 | // well: Be a window manager. 5 | #![warn(clippy::pedantic)] 6 | // Each of these lints are globally allowed because they otherwise make a lot 7 | // of noise. However, work to ensure that each use of one of these is correct 8 | // would be very much appreciated. 9 | #![allow( 10 | clippy::cast_precision_loss, 11 | clippy::cast_possible_truncation, 12 | clippy::cast_possible_wrap, 13 | clippy::cast_sign_loss, 14 | clippy::must_use_candidate, 15 | clippy::default_trait_access 16 | )] 17 | #[macro_use] 18 | extern crate serde_derive; 19 | 20 | pub mod errors; 21 | pub mod models; 22 | pub mod operations; 23 | pub mod utils; 24 | 25 | use colored::Colorize; 26 | use errors::{LeftErrorKind, Result}; 27 | 28 | use crate::models::Config; 29 | use crate::operations::{ 30 | Apply, Current, Install, List, Migrate, New, Search, Status, Support, Uninstall, Update, 31 | Upgrade, 32 | }; 33 | use clap::Parser; 34 | use log::error; 35 | use std::env; 36 | 37 | #[derive(Parser, Debug)] 38 | #[clap(author, about, version)] 39 | pub struct Opt { 40 | /// Verbose mode (-v, -vv, -vvv, etc.) 41 | #[clap(short, long, action = clap::ArgAction::Count)] 42 | pub verbose: u8, 43 | /// Operation to be performed by the theme manager 44 | #[clap(subcommand)] 45 | pub operation: Operation, 46 | } 47 | 48 | #[derive(Parser, Debug)] 49 | pub enum Operation { 50 | // /// Finds themes not installed by LeftWM-theme 51 | //AutoFind(AutoFind), 52 | /// Install a theme 53 | Install(Install), 54 | /// Uninstall a theme 55 | Uninstall(Uninstall), 56 | /// List installed theme(s) 57 | #[clap(name = "list")] 58 | List(List), 59 | /// Migrate `theme.toml` to `theme.ron` 60 | Migrate(Migrate), 61 | /// Create new theme 62 | New(New), 63 | /// Update installed themes 64 | Upgrade(Upgrade), 65 | /// Update theme list 66 | Update(Update), 67 | /// Apply an already installed theme 68 | Apply(Apply), 69 | /// Print out current theme information 70 | Status(Status), 71 | /// Search for a theme by name 72 | Search(Search), 73 | /// Get support (xdg-open) 74 | Support(Support), 75 | /// Get a field from the theme.toml 76 | Current(Current), 77 | } 78 | 79 | fn main() { 80 | let opt = Opt::parse(); 81 | 82 | match opt.verbose { 83 | 0 => env::set_var("RUST_LOG", "warn"), 84 | 1 => env::set_var("RUST_LOG", "info"), 85 | 2 => env::set_var("RUST_LOG", "debug"), 86 | _ => env::set_var("RUST_LOG", "trace"), 87 | } 88 | 89 | pretty_env_logger::init(); 90 | 91 | log::trace!("Loading configuration"); 92 | let mut config = Config::new(None).load().unwrap_or_default(); 93 | 94 | let wrapper: Result<()> = match opt.operation { 95 | //Operation::AutoFind(args) => AutoFind::exec(&args), 96 | Operation::Install(args) => Install::exec(&args, &mut config), 97 | Operation::Uninstall(args) => Uninstall::exec(&args, &mut config), 98 | Operation::List(args) => List::exec(&args, &mut config), 99 | Operation::Apply(args) => Apply::exec(&args, &mut config), 100 | Operation::Status(args) => Status::exec(&args, &mut config), 101 | Operation::Migrate(args) => Migrate::exec(&args), 102 | Operation::New(args) => New::exec(&args, &mut config), 103 | Operation::Upgrade(args) => Upgrade::exec(&args, &mut config), 104 | Operation::Update(args) => Update::exec(&args, &mut config), 105 | Operation::Search(args) => Search::exec(&args, &mut config), 106 | Operation::Support(args) => Support::exec(&args, &mut config), 107 | Operation::Current(args) => Current::exec(&args, &mut config), 108 | }; 109 | 110 | if let Err(e) = wrapper { 111 | if let LeftErrorKind::UserFriendlyError(msg) = e.inner { 112 | println!("{}", &msg.bright_red()); 113 | } else { 114 | error!("Operation did not complete successfully"); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/models/config.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::errors::Result; 3 | use crate::models::theme::{TempThemes, Theme}; 4 | use colored::Colorize; 5 | use log::{error, trace}; 6 | use std::ffi::OsStr; 7 | use std::fs; 8 | use std::fs::File; 9 | use std::io::Write; 10 | use std::path::{Path, PathBuf}; 11 | use xdg::BaseDirectories; 12 | 13 | pub const THEMES_DIR: &str = "themes"; 14 | 15 | const BASE_DIR_PREFIX: &str = "leftwm"; 16 | const CURRENT_DIR: &str = "current"; 17 | const LOCAL_REPO_NAME: &str = "LOCAL"; 18 | const COMMUNITY_REPO_NAME: &str = "community"; 19 | const THEMES_CONFIG_FILENAME: &str = "themes.toml"; 20 | pub const CURRENT_DEFINITIONS_VERSION: i16 = 1; 21 | 22 | /// Contains a vector of all global repositories. 23 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 24 | pub struct Config { 25 | pub repos: Vec, 26 | pub config_dir: Option, 27 | } 28 | 29 | /// Contains global repository information. Akin to known.toml or themes.toml 30 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 31 | pub struct Repo { 32 | pub url: String, 33 | pub name: String, 34 | #[serde(default)] 35 | pub definitions_version: i16, 36 | pub themes: Vec, 37 | } 38 | 39 | impl Config { 40 | #[must_use] 41 | // Create a new Config at the given path. 42 | pub fn new(config_path: Option) -> Self { 43 | Config { 44 | repos: vec![], 45 | config_dir: config_path, 46 | } 47 | } 48 | 49 | #[must_use] 50 | // Populates the Config with defaults and returns it. 51 | pub fn default(&mut self) -> Self { 52 | let community_repo = Repo { 53 | url: String::from("https://raw.githubusercontent.com/leftwm/leftwm-community-themes/master/known.toml"), 54 | name: String::from(COMMUNITY_REPO_NAME), 55 | definitions_version: 1, 56 | themes: Vec::new() 57 | }; 58 | let local_repo = Repo { 59 | url: String::from("localhost"), 60 | name: String::from(LOCAL_REPO_NAME), 61 | definitions_version: CURRENT_DEFINITIONS_VERSION, 62 | themes: Vec::new(), 63 | }; 64 | self.repos.push(community_repo); 65 | self.repos.push(local_repo); 66 | self.clone() 67 | } 68 | 69 | // Returns the config dir path. If config path is None, it constructs and 70 | // returns a default config path (~/.config/leftwm). 71 | /// # Errors 72 | /// 73 | /// Will error if base directory cannot be obtained AND path does not exist on passthru. 74 | /// Will error if unable to create configuration directory. 75 | pub fn get_config_dir(&self) -> Result { 76 | match &self.config_dir { 77 | Some(path) => Ok(path.clone()), 78 | None => { 79 | let path = BaseDirectories::with_prefix(BASE_DIR_PREFIX)?; 80 | // Create the directory if it doesn't exist 81 | fs::create_dir_all(path.get_config_home())?; 82 | Ok(path.get_config_home()) 83 | } 84 | } 85 | } 86 | 87 | /// # Errors 88 | /// 89 | /// Errors if toml cannot be obtained for the themes.toml file 90 | /// Errors if the `BaseDirectory` is not set (no systemd) 91 | /// Errors if no file can be saved 92 | pub fn save(config: &Self) -> Result<&Config> { 93 | let config_filename = config.get_config_dir()?.join(THEMES_CONFIG_FILENAME); 94 | let toml = toml::to_string(&config)?; 95 | let mut file = File::create(config_filename)?; 96 | file.write_all(toml.as_bytes())?; 97 | Ok(config) 98 | } 99 | 100 | pub fn update_or_append(config: &mut Self, theme: &Theme, repo: (&String, &String)) { 101 | #![allow(clippy::option_if_let_else)] 102 | if let Some(target_repo) = config.repos.iter_mut().find(|p| repo.1.clone() == p.name) { 103 | match target_repo.themes.iter_mut().find(|o| theme.name == o.name) { 104 | Some(target_theme) => { 105 | // If there is one, update values 106 | target_theme.repository = theme.repository.clone(); 107 | target_theme.description = theme.description.clone(); 108 | target_theme.support_url = theme.support_url.clone(); 109 | target_theme.commit = theme.commit.clone(); 110 | target_theme.version = theme.version.clone(); 111 | target_theme.leftwm_versions = theme.leftwm_versions.clone(); 112 | target_theme.dependencies = theme.dependencies.clone(); 113 | } 114 | None => { 115 | target_repo.themes.push(theme.clone()); 116 | } 117 | } 118 | } 119 | // o/w insert a new leaf at the end 120 | else { 121 | config.repos.push(Repo { 122 | url: repo.0.clone(), 123 | name: repo.1.clone(), 124 | themes: Vec::new(), 125 | definitions_version: CURRENT_DEFINITIONS_VERSION, 126 | }); 127 | let lent = config.repos.len(); 128 | config.repos[lent - 1].themes.push(theme.clone()); 129 | } 130 | } 131 | 132 | pub fn themes(&mut self, local: bool) -> Vec { 133 | let mut themes: Vec = Vec::new(); 134 | for repo in &self.repos { 135 | if local && repo.name == *LOCAL_REPO_NAME { 136 | continue; 137 | } 138 | for theme in &repo.themes { 139 | themes.push(theme.clone().source(repo.name.clone()).clone()); 140 | } 141 | } 142 | themes 143 | } 144 | 145 | /// # Errors 146 | /// 147 | /// Will error if `BaseDirectory` not set (no systemd) 148 | /// Will error if themes.toml doesn't exist 149 | /// Will error if themes.toml has invalid content. 150 | /// Will error if themes.toml cannot be written to. 151 | pub fn load(&self) -> Result { 152 | let config_filename = self.get_config_dir()?.join(THEMES_CONFIG_FILENAME); 153 | if Path::new(&config_filename).exists() { 154 | let contents = fs::read_to_string(config_filename)?; 155 | trace!("{:?}", &contents); 156 | match toml::from_str::(&contents) { 157 | Ok(config) => Ok(config), 158 | Err(err) => { 159 | error!("TOML error: {:?}", err); 160 | Err(errors::LeftError::from("TOML Invalid")) 161 | } 162 | } 163 | } else { 164 | let config = Config::new(None).default(); 165 | let toml = toml::to_string(&config)?; 166 | let mut file = File::create(&config_filename)?; 167 | file.write_all(toml.as_bytes())?; 168 | Ok(config) 169 | } 170 | } 171 | 172 | // Updates the Config with local themes. This depends on the config to 173 | // already have remote theme repos populated. It assumes that the themes 174 | // that aren't in any of the remote repos are local themes. 175 | // 176 | /// # Errors 177 | /// 178 | /// Will return an error if the installed themes listing cannot be obtained. 179 | pub fn update_local_repo(&mut self) -> Result<()> { 180 | // Get a list of all the themes in the themes directory. 181 | let existing_themes = Repo::installed_themes(&self.get_config_dir()?)?; 182 | 183 | let mut local_themes: Vec = Vec::new(); 184 | 185 | // Iterate through the existing themes and check if they are from the 186 | // remote repos. If not, consider the theme to be a local theme. 187 | for tt in existing_themes { 188 | let mut found: bool = false; 189 | for repo in &self.repos { 190 | if repo.name != LOCAL_REPO_NAME { 191 | for theme in &repo.themes { 192 | if tt.eq(&theme.name) { 193 | found = true; 194 | break; 195 | } 196 | } 197 | } 198 | // Break out of the loop, since we already found the theme, 199 | // so as to process the next existing theme. 200 | if found { 201 | break; 202 | } 203 | } 204 | if !found { 205 | local_themes.push(tt); 206 | } 207 | } 208 | 209 | // Create TempThemes from the local themes. 210 | let mut local_temp_themes = TempThemes { 211 | theme: vec![], 212 | definitions_version: CURRENT_DEFINITIONS_VERSION, 213 | }; 214 | let config_dir = self.get_config_dir()?; 215 | for lt in local_themes { 216 | let path = config_dir.clone().join(THEMES_DIR).join(<); 217 | let t = Theme::new(<, None, Some(path)); 218 | local_temp_themes.theme.push(t); 219 | } 220 | 221 | // Update the local themes in the Config. 222 | for repo in &mut self.repos { 223 | if repo.name == LOCAL_REPO_NAME { 224 | repo.compare(local_temp_themes, &config_dir)?; 225 | break; 226 | } 227 | } 228 | 229 | Ok(()) 230 | } 231 | } 232 | 233 | impl Repo { 234 | /// # Errors 235 | /// 236 | /// Returns an error if the definitions file is OOD. 237 | pub fn compare(&mut self, theme_wrap: TempThemes, config_dir: &Path) -> Result<&Repo> { 238 | if self.definitions_version > CURRENT_DEFINITIONS_VERSION 239 | || theme_wrap.definitions_version > CURRENT_DEFINITIONS_VERSION 240 | { 241 | println!("{}", "========== ERROR ==========".bold().red()); 242 | println!("REPOSITORY DEFINITION HAS INCREASED."); 243 | println!("USUALLY THIS MEANS YOUR LEFTWM-THEME IS OUT OF DATE."); 244 | println!("{}", "========== ERROR ==========".bold().red()); 245 | return Err(errors::LeftError::from("Definitions file out of date.")); 246 | } 247 | let themes = theme_wrap.theme; 248 | trace!("Comparing themes"); 249 | 250 | // Get a list of existing themes. 251 | let existing_themes = Repo::installed_themes(config_dir)?; 252 | let current_theme = Repo::current_theme(config_dir)?.unwrap_or_default(); 253 | let themes_dir = config_dir.join(THEMES_DIR); 254 | 255 | // Iterate over all the themes, and update/add if needed. 256 | for mut tema in themes { 257 | // Apply any theme changes before updating or adding it. 258 | tema.apply_changes(config_dir)?; 259 | 260 | // Check if the theme is already installed and update the theme 261 | // directory attribute. 262 | if existing_themes.contains(&tema.name.clone()) { 263 | tema.directory = Some(themes_dir.join(tema.name.clone())); 264 | } 265 | 266 | // Check if this is the current theme. 267 | if current_theme.eq(&tema.name.clone()) { 268 | tema.current = Some(true); 269 | } 270 | 271 | Repo::update_or_append(self, &tema); 272 | } 273 | Ok(self) 274 | } 275 | 276 | pub fn update_or_append(repo: &mut Self, theme: &Theme) { 277 | let name = repo.name.clone(); 278 | trace!("{:?} in {:?}", &theme, &name); 279 | match repo 280 | .themes 281 | .iter_mut() 282 | .find(|p| theme.name.clone() == p.name.clone()) 283 | { 284 | Some(target_theme) => { 285 | // If there is one, update values 286 | target_theme.repository = theme.repository.clone(); 287 | target_theme.description = theme.description.clone(); 288 | target_theme.commit = theme.commit.clone(); 289 | target_theme.version = theme.version.clone(); 290 | target_theme.leftwm_versions = theme.leftwm_versions.clone(); 291 | target_theme.support_url = theme.support_url.clone(); 292 | target_theme.set_relative_directory(theme.relative_directory.clone()); 293 | target_theme.dependencies = theme.dependencies.clone(); 294 | target_theme.directory = theme.directory.clone(); 295 | } 296 | // o/w insert a new leaf at the end 297 | None => { 298 | repo.themes.push(theme.clone()); 299 | } 300 | } 301 | } 302 | 303 | // Looks for the current theme in the themes directory and returns the name 304 | // of the current theme. 305 | fn current_theme(config_path: &Path) -> Result> { 306 | let theme_path = config_path.join(THEMES_DIR); 307 | 308 | // Return None if themes directory doesn't exist. 309 | if !theme_path.exists() { 310 | return Ok(None); 311 | } 312 | 313 | // Read the themes directory, find the "current" theme and get the 314 | // current theme name. 315 | let mut result = String::new(); 316 | let current_dir = OsStr::new(CURRENT_DIR); 317 | let paths = fs::read_dir(theme_path)?; 318 | for path in paths { 319 | let p = &path?.path(); 320 | // Get the file with name "current" and check if it's a symlink. 321 | // Follow the symlink to find the target theme. 322 | let target_file_name = p 323 | .file_name() 324 | .unwrap_or_default() 325 | .to_str() 326 | .unwrap_or_default(); 327 | if target_file_name.eq(current_dir) { 328 | let metadata = fs::symlink_metadata(p)?; 329 | let file_type = metadata.file_type(); 330 | if file_type.is_symlink() { 331 | result = String::from( 332 | fs::read_link(p)? 333 | .file_name() 334 | .unwrap_or_default() 335 | .to_str() 336 | .unwrap_or_default(), 337 | ); 338 | } 339 | break; 340 | } 341 | } 342 | 343 | if result.is_empty() { 344 | return Ok(None); 345 | } 346 | 347 | Ok(Some(result)) 348 | } 349 | 350 | // Returns a list of all the installed theme names under a given config 351 | // path. 352 | fn installed_themes(config_path: &Path) -> Result> { 353 | let mut result: Vec = Vec::new(); 354 | 355 | let theme_path = config_path.join(THEMES_DIR); 356 | 357 | // Return empty result if the themes directory is not present. 358 | if !theme_path.exists() { 359 | return Ok(result); 360 | } 361 | 362 | // Read the themes directory, iterate through the entries, determine 363 | // which of them are theme directories and add them into the result. 364 | let paths = fs::read_dir(theme_path)?; 365 | for path in paths { 366 | let p = path?; 367 | // NOTE: For symlinks, metadata() traverses any symlinks and queries 368 | // the metadata information from the destination. 369 | let metadata = fs::metadata(p.path())?; 370 | let file_type = metadata.file_type(); 371 | 372 | // Only process directories. 373 | if !file_type.is_dir() { 374 | continue; 375 | } 376 | 377 | // Ignore the "current" directory for installed theme list. 378 | let current_dir = String::from(CURRENT_DIR); 379 | let target_path = p.path(); 380 | if target_path 381 | .file_name() 382 | .unwrap_or_default() 383 | .to_str() 384 | .unwrap_or_default() 385 | .eq(¤t_dir) 386 | { 387 | continue; 388 | } 389 | 390 | // Extract only the theme name for the result. 391 | let theme_name = target_path.file_name().unwrap_or_default(); 392 | result.push(String::from(theme_name.to_str().unwrap_or_default())); 393 | } 394 | 395 | Ok(result) 396 | } 397 | } 398 | 399 | #[cfg(test)] 400 | mod test { 401 | use super::*; 402 | use std::os::unix::fs as unix_fs; 403 | 404 | #[test] 405 | fn test_installed_themes() { 406 | // Create a temporary directory as the config path and create the 407 | // directory layout within it for themes. 408 | let tmpdir = tempfile::tempdir().unwrap(); 409 | let themes_dir = tmpdir.path().join(THEMES_DIR); 410 | let theme1 = themes_dir.join("test-theme1"); 411 | let theme2 = themes_dir.join("test-theme2"); 412 | let unrelated_file = themes_dir.join("some-file"); 413 | assert!(fs::create_dir_all(theme1).is_ok()); 414 | assert!(fs::create_dir_all(&theme2).is_ok()); 415 | assert!(File::create(unrelated_file).is_ok()); 416 | 417 | // Create current theme as a symlink to an existing theme. 418 | let current = themes_dir.join(CURRENT_DIR); 419 | let src = theme2.to_str().unwrap(); 420 | let dst = current.to_str().unwrap(); 421 | assert!(unix_fs::symlink(src, dst).is_ok()); 422 | 423 | let config_dir = tmpdir.path().to_path_buf(); 424 | let result = Repo::installed_themes(&config_dir); 425 | assert!(result.is_ok()); 426 | let mut result_vec = result.unwrap(); 427 | result_vec.sort(); 428 | assert_eq!( 429 | result_vec, 430 | vec!["test-theme1".to_string(), "test-theme2".to_string(),], 431 | ); 432 | } 433 | 434 | #[test] 435 | fn test_installed_themes_no_themes_dir() { 436 | let tmpdir = tempfile::tempdir().unwrap(); 437 | let config_dir = tmpdir.path().to_path_buf(); 438 | assert!(Repo::installed_themes(&config_dir).is_ok()); 439 | } 440 | 441 | #[test] 442 | fn test_current_theme() { 443 | // Create a temporary directory as the config path and create the 444 | // directory layout within it for themes. 445 | let tmpdir = tempfile::tempdir().unwrap(); 446 | let themes_dir = tmpdir.path().join(THEMES_DIR); 447 | let theme1 = themes_dir.join("test-theme1"); 448 | let theme2 = themes_dir.join("test-theme2"); 449 | assert!(fs::create_dir_all(theme1).is_ok()); 450 | assert!(fs::create_dir_all(&theme2).is_ok()); 451 | 452 | // Create current theme as a symlink to an existing theme. 453 | let current = themes_dir.join(CURRENT_DIR); 454 | let src = theme2.to_str().unwrap(); 455 | let dst = current.to_str().unwrap(); 456 | assert!(unix_fs::symlink(src, dst).is_ok()); 457 | 458 | let result = Repo::current_theme(tmpdir.path()); 459 | assert_eq!(result.unwrap().unwrap(), "test-theme2"); 460 | } 461 | 462 | #[test] 463 | fn test_current_theme_unmanaged() { 464 | let tmpdir = tempfile::tempdir().unwrap(); 465 | let themes_dir = tmpdir.path().join(THEMES_DIR); 466 | 467 | // Custom theme, not a symlink, not managed by leftwm-theme. 468 | let current = themes_dir.join(CURRENT_DIR); 469 | assert!(fs::create_dir_all(current).is_ok()); 470 | 471 | let result = Repo::current_theme(tmpdir.path()); 472 | assert!(result.unwrap().is_none()); 473 | } 474 | 475 | #[test] 476 | fn test_current_theme_no_themes_dir() { 477 | let tmpdir = tempfile::tempdir().unwrap(); 478 | assert!(Repo::current_theme(tmpdir.path()).unwrap().is_none()); 479 | } 480 | 481 | #[test] 482 | fn test_current_theme_no_current() { 483 | let tmpdir = tempfile::tempdir().unwrap(); 484 | let themes_dir = tmpdir.path().join(THEMES_DIR); 485 | let theme1 = themes_dir.join("test-theme1"); 486 | let theme2 = themes_dir.join("test-theme2"); 487 | assert!(fs::create_dir_all(theme1).is_ok()); 488 | assert!(fs::create_dir_all(theme2).is_ok()); 489 | assert!(Repo::current_theme(tmpdir.path()).unwrap().is_none()); 490 | } 491 | 492 | #[test] 493 | fn test_current_theme_current_file() { 494 | let tmpdir = tempfile::tempdir().unwrap(); 495 | let themes_dir = tmpdir.path().join(THEMES_DIR); 496 | assert!(fs::create_dir_all(&themes_dir).is_ok()); 497 | 498 | // Create a file "current", instead of a directory. 499 | let current_file = themes_dir.join(CURRENT_DIR); 500 | assert!(File::create(current_file).is_ok()); 501 | 502 | assert!(Repo::current_theme(tmpdir.path()).unwrap().is_none()); 503 | } 504 | 505 | #[test] 506 | fn test_config_new() { 507 | let config1 = Config::new(None); 508 | assert!(config1.config_dir.is_none()); 509 | assert!(config1 510 | .get_config_dir() 511 | .unwrap() 512 | .to_str() 513 | .unwrap() 514 | .ends_with("/.config/leftwm/")); 515 | 516 | let config2 = Config::new(Some(PathBuf::from("/tmp/foo"))); 517 | assert!(config2.config_dir.is_some()); 518 | assert!(config2 519 | .get_config_dir() 520 | .unwrap() 521 | .to_str() 522 | .unwrap() 523 | .eq("/tmp/foo")); 524 | } 525 | 526 | #[test] 527 | fn test_config_default() { 528 | let config = Config::new(None).default(); 529 | assert_eq!(config.repos.len(), 2); 530 | assert!(config.repos.iter().any(|x| x.name == COMMUNITY_REPO_NAME)); 531 | assert!(config.repos.iter().any(|x| x.name == LOCAL_REPO_NAME)); 532 | } 533 | 534 | #[test] 535 | fn test_config_update_local_repo() { 536 | // Create test config directory layout with community and local themes. 537 | let tmpdir = tempfile::tempdir().unwrap(); 538 | let themes_dir = tmpdir.path().join(THEMES_DIR); 539 | let comm_theme1 = themes_dir.join("community-theme1"); 540 | let comm_theme2 = themes_dir.join("community-theme2"); 541 | let local_theme1 = themes_dir.join("local-theme1"); 542 | let local_theme2 = themes_dir.join("local-theme2"); 543 | 544 | assert!(fs::create_dir_all(&comm_theme1).is_ok()); 545 | assert!(fs::create_dir_all(&comm_theme2).is_ok()); 546 | assert!(fs::create_dir_all(&local_theme1).is_ok()); 547 | assert!(fs::create_dir_all(local_theme2).is_ok()); 548 | 549 | // Set current theme symlink to a local theme. 550 | let current = themes_dir.join(CURRENT_DIR); 551 | let src = local_theme1.to_str().unwrap(); 552 | let dst = current.to_str().unwrap(); 553 | assert!(unix_fs::symlink(src, dst).is_ok()); 554 | 555 | // Construct themes to be added to the community repo. 556 | let t1 = Theme::new("community-theme1", None, Some(comm_theme1)); 557 | let t2 = Theme::new("community-theme2", None, Some(comm_theme2)); 558 | // Extra uninstalled theme. 559 | let t3 = Theme::new("community-theme3", None, None); 560 | 561 | let mut config = Config::new(Some(tmpdir.path().to_path_buf())).default(); 562 | 563 | // Append the themes created above to the community repo. 564 | for repo in &mut config.repos { 565 | if repo.name == COMMUNITY_REPO_NAME { 566 | repo.themes.push(t1); 567 | repo.themes.push(t2); 568 | repo.themes.push(t3); 569 | break; 570 | } 571 | } 572 | 573 | assert!(config.update_local_repo().is_ok()); 574 | 575 | let comm_repo = config 576 | .repos 577 | .clone() 578 | .into_iter() 579 | .find(|x| x.name == COMMUNITY_REPO_NAME) 580 | .unwrap(); 581 | assert_eq!(comm_repo.themes.len(), 3); 582 | 583 | let local_repo = config 584 | .repos 585 | .into_iter() 586 | .find(|x| x.name == LOCAL_REPO_NAME) 587 | .unwrap(); 588 | assert_eq!(local_repo.themes.len(), 2); 589 | 590 | // Check if local theme is the current theme. 591 | let local_theme1 = local_repo 592 | .themes 593 | .into_iter() 594 | .find(|x| x.name == "local-theme1") 595 | .unwrap(); 596 | assert!(local_theme1.current.unwrap()); 597 | } 598 | } 599 | -------------------------------------------------------------------------------- /src/models/leftwm.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{LeftError, Result}; 2 | use std::process::Command; 3 | use std::str; 4 | 5 | #[derive(Debug)] 6 | pub struct LeftWm { 7 | pub version: String, 8 | } 9 | 10 | impl LeftWm { 11 | /// # Errors 12 | /// 13 | /// Will error if output of leftwm-state fails to be obtained. 14 | /// Will error if leftwm-state output can't be parsed 15 | pub fn get() -> Result { 16 | let version_raw = &Command::new("leftwm-state").arg("-V").output()?.stdout; 17 | let version_utf8 = str::from_utf8(version_raw).map_err(|_| LeftError::from("UTF Error"))?; 18 | let version = version_utf8.replace("LeftWM State ", "").replace('\n', ""); 19 | Ok(LeftWm { version }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod leftwm; 3 | mod theme; 4 | 5 | pub use config::{Config, Repo, THEMES_DIR}; 6 | pub use leftwm::LeftWm; 7 | pub use theme::{DependencyL, Theme}; 8 | -------------------------------------------------------------------------------- /src/models/theme.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::{Config, THEMES_DIR}; 3 | use std::fs; 4 | use std::path::{Path, PathBuf}; 5 | 6 | /// Contains information about a theme contained within themes.toml (or known.toml upstream). 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub struct Theme { 9 | /// Name of the theme, must follow arch convention [az-_] 10 | pub name: String, 11 | /// A helpful description of the theme 12 | pub description: Option, 13 | /// (Local) (Managed by leftwm-theme), the local directory where the theme is stored 14 | pub directory: Option, 15 | /// The git repository where the theme may be downloaded from 16 | pub repository: Option, 17 | /// The commit to use for the theme; can use * for HEAD 18 | pub commit: Option, 19 | /// The version for the theme, incrementing will force updates 20 | pub version: Option, 21 | /// Compatible leftwm versions 22 | pub leftwm_versions: Option, 23 | /// (Local) Whether the theme is the current theme 24 | pub current: Option, 25 | /// A list of dependencies 26 | pub dependencies: Option>, 27 | /// Path to the directory containing up, down, and theme.toml w.r.t. root 28 | pub relative_directory: Option, 29 | /// URL to the help resources for this theme 30 | pub support_url: Option, 31 | #[serde(skip)] 32 | pub source: Option, 33 | } 34 | 35 | /// Contains a vector of themes used for processing. 36 | #[derive(Debug, Deserialize)] 37 | pub struct TempThemes { 38 | #[serde(default)] 39 | pub definitions_version: i16, 40 | pub theme: Vec, 41 | } 42 | 43 | impl Default for TempThemes { 44 | fn default() -> TempThemes { 45 | TempThemes { 46 | theme: vec![], 47 | definitions_version: crate::models::config::CURRENT_DEFINITIONS_VERSION, 48 | } 49 | } 50 | } 51 | 52 | /// Contains information pertaining to a program dependency (name, required/optional, package). 53 | #[derive(Debug, Deserialize, Clone, Serialize)] 54 | pub struct DependencyL { 55 | pub program: String, 56 | pub optional: Option, 57 | pub package: Option, 58 | } 59 | 60 | impl Default for DependencyL { 61 | fn default() -> DependencyL { 62 | DependencyL { 63 | program: String::from("leftwm"), 64 | optional: None, 65 | package: None, 66 | } 67 | } 68 | } 69 | 70 | impl Theme { 71 | #[must_use] 72 | pub fn new(name: &str, description: Option, directory: Option) -> Self { 73 | Theme { 74 | name: name.to_string(), 75 | description, 76 | directory, 77 | repository: None, 78 | commit: None, 79 | version: Some("0.0.0".to_string()), 80 | leftwm_versions: Some("*".to_string()), 81 | dependencies: None, 82 | current: Some(false), 83 | relative_directory: None, 84 | support_url: None, 85 | source: None, 86 | } 87 | } 88 | 89 | #[must_use] 90 | pub fn is_installed(&self) -> bool { 91 | match &self.directory { 92 | Some(_dir) => true, 93 | None => false, 94 | } 95 | } 96 | 97 | pub fn find(config: &mut Config, name: &str) -> Option { 98 | config 99 | .themes(false) 100 | .iter() 101 | .find(|p| name == p.name) 102 | .cloned() 103 | } 104 | 105 | pub fn find_installed(config: &mut Config, name: &str) -> Option { 106 | config 107 | .themes(false) 108 | .iter() 109 | .find(|p| name == p.name && p.directory.is_some()) 110 | .cloned() 111 | } 112 | 113 | pub fn find_all(config: &mut Config, name: &str) -> Option> { 114 | let (themes, _) = config 115 | .themes(false) 116 | .iter() 117 | .cloned() 118 | .partition::, _>(|p| name == p.name); 119 | Some(themes) 120 | } 121 | 122 | pub fn find_mut<'a>( 123 | config: &'a mut Config, 124 | name: &str, 125 | repo_name: &str, 126 | ) -> Option<&'a mut Theme> { 127 | match config.repos.iter_mut().find(|p| repo_name == p.name) { 128 | Some(reposit) => reposit.themes.iter_mut().find(|o| name == o.name), 129 | None => None, 130 | } 131 | } 132 | 133 | pub fn source(&mut self, name: String) -> &mut Theme { 134 | self.source = Some(name); 135 | self 136 | } 137 | 138 | /// Sets relative directory; abstracting because behavior might change 139 | pub fn set_relative_directory(&mut self, rel_dir: Option) { 140 | self.relative_directory = rel_dir; 141 | } 142 | 143 | /// Gets relative directory; abstracting because behavior might change; <3 JKN MGK 144 | pub fn relative_directory(&self) -> Option { 145 | self.relative_directory.clone() 146 | } 147 | 148 | pub fn current(&mut self, currency: bool) { 149 | self.current = if currency { Some(true) } else { None } 150 | } 151 | 152 | /// Gets the name of the theme after applying any theme changes. 153 | pub fn get_name(&self) -> String { 154 | // TODO: When theme rename change is implemented, return the newly 155 | // obtained name after applying the rename change if any. 156 | self.name.clone() 157 | } 158 | 159 | /// Applies changes to the repo if changes are defined for it. 160 | /// # Errors 161 | /// 162 | /// Errors if the directory rename change fails. 163 | pub fn apply_changes(&self, config_dir: &Path) -> Result<(), errors::LeftError> { 164 | let effectual_name = self.get_name(); 165 | if self.name != effectual_name { 166 | Theme::apply_change_rename_dir(&self.name, &effectual_name, config_dir)?; 167 | } 168 | Ok(()) 169 | } 170 | 171 | // Applies the theme rename change. Given an old name and a new name of the 172 | // theme, it renames the theme's old directory to the new name if found. 173 | fn apply_change_rename_dir( 174 | old_name: &str, 175 | new_name: &str, 176 | config_dir: &Path, 177 | ) -> Result<(), errors::LeftError> { 178 | let old_theme_dir = config_dir.join(THEMES_DIR).join(old_name); 179 | if old_theme_dir.exists() { 180 | println!("Moving theme {old_name} to {new_name}"); 181 | let new_theme_dir = config_dir.join(THEMES_DIR).join(new_name); 182 | fs::rename(old_theme_dir, new_theme_dir)?; 183 | } 184 | Ok(()) 185 | } 186 | } 187 | 188 | #[cfg(test)] 189 | mod test { 190 | use super::*; 191 | 192 | #[test] 193 | fn test_apply_change_rename_dir_existing_theme() { 194 | let tmpdir = tempfile::tempdir().unwrap(); 195 | let themes_dir = tmpdir.path().join(THEMES_DIR); 196 | let old_theme_dir = themes_dir.join("theme-x"); 197 | let new_theme_dir = themes_dir.join("theme-y"); 198 | 199 | // Create old theme dir. 200 | assert!(fs::create_dir_all(old_theme_dir).is_ok()); 201 | 202 | assert!(Theme::apply_change_rename_dir("theme-x", "theme-y", &tmpdir.into_path()).is_ok()); 203 | 204 | // Check if the new dir exists. 205 | assert!(new_theme_dir.exists()); 206 | } 207 | 208 | #[test] 209 | fn test_apply_change_rename_dir_no_existing_theme() { 210 | let tmpdir = tempfile::tempdir().unwrap(); 211 | let themes_dir = tmpdir.path().join(THEMES_DIR); 212 | let new_theme_dir = themes_dir.join("theme-y"); 213 | 214 | // No theme directory exists. 215 | 216 | assert!(Theme::apply_change_rename_dir("theme-x", "theme-y", &tmpdir.into_path()).is_ok()); 217 | assert!(!new_theme_dir.exists()); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/operations/apply.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{Config, Theme}; 2 | use crate::{errors, utils}; 3 | use clap::Parser; 4 | use colored::Colorize; 5 | use errors::LeftError; 6 | use log::{error, trace, warn}; 7 | use std::os::unix; 8 | use std::path::Path; 9 | use std::process::Command; 10 | use std::{env, fs}; 11 | use xdg::BaseDirectories; 12 | 13 | /* This function sets a particular theme as the current theme in ~./config/leftwm/themes/ 14 | Required args include "THEME", which defines the NAME of a theme as defined in a known.toml file or the themes.toml file in ~/.config/leftwm/ 15 | TODO: THEME (with the -g/git or -f/folder flags) may also point to a git url (in the future) with a defined theme.toml file with enough global parameters defined to embed the theme in themes.toml 16 | Possible optional args include debug, which prints all trace! commands, and no-reset, which prevents leftwm-theme from resetting the theme 17 | */ 18 | 19 | #[derive(Parser, Debug)] 20 | pub struct Apply { 21 | pub name: String, 22 | 23 | /// Don't restart leftwm-worker 24 | #[clap(short = 'n', long)] 25 | pub no_reset: bool, 26 | 27 | /// Ignore checks 28 | #[clap(short = 'o', long)] 29 | pub override_checks: bool, 30 | } 31 | 32 | impl Apply { 33 | /// # Errors 34 | /// 35 | /// Returns an error if config cannot be loaded / saved 36 | /// Returns an error if `BaseDirectory` not set. 37 | /// Returns an error if symlink cannot be made. 38 | /// Returns an error if theme not found. 39 | /// Returns an error if leftwm-worker cannot be killed. 40 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 41 | trace!("Applying theme named {:?}", &self.name); 42 | println!( 43 | "{}{}{}", 44 | "Setting ".bright_blue().bold(), 45 | &self.name.bright_green().bold(), 46 | " as default theme.".bright_blue().bold() 47 | ); 48 | let mut dir = BaseDirectories::with_prefix("leftwm")?.create_config_directory("")?; 49 | dir.push("themes"); 50 | dir.push("current"); 51 | trace!("{:?}", &dir); 52 | if let Some(theme) = Theme::find(config, &self.name) { 53 | if let Some(theme_dir) = theme.directory.as_ref() { 54 | //Do all necessary checks 55 | let checks_data = checks(&theme); 56 | if !checks_data && !self.override_checks { 57 | error!("Not all prerequirements passed"); 58 | return Err(errors::LeftError::from("PreReqs")); 59 | } else if !checks_data && self.override_checks { 60 | warn!("Installing theme despite errors"); 61 | } 62 | let mut path = Path::new(theme_dir).to_path_buf(); 63 | if let Some(rel_dir) = theme.relative_directory() { 64 | path.push(rel_dir); 65 | } 66 | trace!("{:?}", &path); 67 | match fs::remove_dir_all(&dir) { 68 | Ok(()) => { 69 | warn!("Removed old current directory"); 70 | } 71 | Err(_) => { 72 | trace!("Nothing needed removed"); 73 | } 74 | } 75 | unix::fs::symlink(path, dir)?; 76 | println!( 77 | "{}{}{}", 78 | "Applying ".bright_blue().bold(), 79 | &self.name.bright_green().bold(), 80 | " as default theme.".bright_blue().bold() 81 | ); 82 | trace!("{:?}", "Altering config"); 83 | for repo in &mut config.repos { 84 | for theme in &mut repo.themes { 85 | theme.current = Some(false); 86 | } 87 | } 88 | if let Some(source) = theme.source { 89 | if let Some(target_theme) = Theme::find_mut(config, &theme.name, &source) { 90 | target_theme.current(true); 91 | } else { 92 | error!("Theme not found"); 93 | return Err(LeftError::from("Theme not found")); 94 | } 95 | } else { 96 | error!("Theme does not have a source"); 97 | } 98 | 99 | Config::save(config)?; 100 | if !self.no_reset { 101 | println!("{}", "Reloading LeftWM.".bright_blue().bold()); 102 | Command::new("leftwm-command").arg("SoftReload").output()?; 103 | } 104 | Ok(()) 105 | } else { 106 | error!( 107 | "\nTheme not installed. Try installing it with `leftwm-theme install {}`.", 108 | &self.name 109 | ); 110 | Err(errors::LeftError::from("Theme not installed")) 111 | } 112 | } else { 113 | error!("\n Theme not installed. Try checking your spelling?"); 114 | Err(errors::LeftError::from("Theme not installed")) 115 | } 116 | } 117 | } 118 | 119 | pub(crate) fn checks(theme: &Theme) -> bool { 120 | trace!("Checking dependencies."); 121 | match theme.dependencies.clone() { 122 | None => { 123 | trace!("No dependencies detected"); 124 | } 125 | Some(theme_dependencies) => { 126 | for dependency in theme_dependencies { 127 | if !is_program_in_path(&dependency.program) { 128 | return false; 129 | } 130 | } 131 | } 132 | } 133 | trace!("Checking LeftWM version."); 134 | if let Ok(true) = utils::versions::check( 135 | &theme 136 | .leftwm_versions 137 | .clone() 138 | .unwrap_or_else(|| "*".to_string()), 139 | ) { 140 | true 141 | } else { 142 | error!("This theme is incompatible with the installed version of LeftWM. \n You may be able to recover this theme, see https://github.com/leftwm/leftwm/wiki/Diagnosing-Theme-Errors"); 143 | false 144 | } 145 | } 146 | 147 | fn is_program_in_path(program: &str) -> bool { 148 | trace!("Checking dependency {}", program); 149 | if let Ok(path) = env::var("PATH") { 150 | for p in path.split(':') { 151 | let p_str = format!("{p}/{program}"); 152 | if fs::metadata(p_str).is_ok() { 153 | return true; 154 | } 155 | } 156 | } 157 | false 158 | } 159 | -------------------------------------------------------------------------------- /src/operations/autofind.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::Config; 3 | use clap::Clap; 4 | use colored::Colorize; 5 | use std::path::Path; 6 | use xdg::BaseDirectories; 7 | 8 | #[derive(Clap, Debug)] 9 | pub struct AutoFind { 10 | /// Optional directory to search for themes defined by a theme.toml / git pair 11 | pub dir: Option, 12 | } 13 | 14 | impl AutoFind { 15 | pub fn exec(&self) -> Result<(), errors::LeftError> { 16 | unimplemented!("Not yet implemented"); 17 | Ok(()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/operations/current.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::errors::friendly_message; 3 | use crate::models::Config; 4 | use clap::Parser; 5 | use std::fs; 6 | use std::path::PathBuf; 7 | use toml::Value; 8 | 9 | #[derive(Parser, Debug)] 10 | pub struct Current { 11 | pub field: String, 12 | } 13 | 14 | impl Current { 15 | /// # Errors 16 | /// 17 | /// Will error if the requested field in not found in theme.toml 18 | /// 19 | /// # Panics 20 | /// 21 | /// Will panic if unable to retrieve value but file does exist. 22 | /// Will panic if a current theme is set but directory is not set. 23 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 24 | // define directory so it can be used outside the scope of the loop 25 | let mut directory: PathBuf = PathBuf::new(); 26 | 27 | // get the path to the current theme.toml 28 | for theme in config.themes(false) { 29 | if theme.current == Some(true) { 30 | directory = theme.directory.unwrap(); 31 | directory.push("theme"); 32 | directory.set_extension("toml"); 33 | } 34 | } 35 | 36 | // read the current theme.toml 37 | let file_data: String = fs::read_to_string(directory).unwrap(); 38 | let cfg_data: Value = toml::from_str(&file_data).expect("no data"); 39 | 40 | // check if the field exists if it doesn't return an error 41 | if let Some(field_value) = cfg_data.get(self.field.clone()) { 42 | //return requested field 43 | println!("{}", format!("{field_value}").replace('\"', "")); 44 | Ok(()) 45 | } else { 46 | // returning an error 47 | Err(friendly_message("That field was not found")) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/operations/install.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::friendly_message; 2 | use crate::errors::Result; 3 | use crate::models::{Config, Theme}; 4 | use crate::{errors, utils}; 5 | use clap::Parser; 6 | use colored::Colorize; 7 | use git2::Repository; 8 | use log::{error, trace}; 9 | use std::io; 10 | use std::io::Write; 11 | 12 | #[derive(Parser, Debug)] 13 | pub struct Install { 14 | /// Read theme from git repository 15 | #[clap(short = 'g', long)] 16 | pub git: bool, 17 | 18 | /// Read theme from path 19 | #[clap(short = 'p', long)] 20 | pub path: bool, 21 | 22 | /// Location of theme 23 | pub name: String, 24 | } 25 | 26 | impl Install { 27 | /// # Errors 28 | /// 29 | /// Will error if config cannot be loaded, or saved. 30 | /// Will error if theme cannot be found. 31 | /// 32 | pub fn exec(&self, mut config: &mut Config) -> Result<()> { 33 | println!("{}", "Looking for theme . . . ".bright_blue().bold()); 34 | trace!("{:?}", &mut config); 35 | 36 | let mut found = Theme::find_all(config, &self.name) 37 | .ok_or_else(|| friendly_message("Could not find theme"))?; 38 | 39 | //ask the user to pick a matching theme 40 | let selected = choose_one(&mut found)?; 41 | 42 | //install the selected theme 43 | self.install_selected_theme(selected, config)?; 44 | 45 | Ok(()) 46 | } 47 | 48 | fn install_selected_theme(&self, theme: &mut Theme, config: &mut Config) -> Result<()> { 49 | trace!("{:?}", &theme); 50 | //get the repo 51 | let repo = theme 52 | .repository 53 | .as_ref() 54 | .ok_or_else(|| friendly_message("Repository information missing for theme"))?; 55 | //build the path 56 | let mut dir = utils::dir::theme()?; 57 | dir.push(&theme.name); 58 | //clone the repo 59 | Repository::clone(repo, dir.clone()).map_err(|err| { 60 | let msg = format!( 61 | "\n{} could not be installed because {:?} \n\n Theme not installed", 62 | &theme.name, 63 | err.message() 64 | ); 65 | friendly_message(&msg) 66 | })?; 67 | // 68 | self.add_to_config_and_save(theme, config, dir) 69 | } 70 | 71 | fn add_to_config_and_save( 72 | &self, 73 | theme: &mut Theme, 74 | config: &mut Config, 75 | dir: std::path::PathBuf, 76 | ) -> Result<()> { 77 | let not_in_db = || friendly_message("Theme not found in db"); 78 | 79 | // update the directory info of theme entry in the config 80 | let source = theme.source.as_ref().ok_or_else(not_in_db)?; 81 | let target_theme = Theme::find_mut(config, &self.name, source).ok_or_else(not_in_db)?; 82 | target_theme.directory = Some(dir); 83 | Config::save(config)?; 84 | 85 | print_theme_install_info(theme); 86 | 87 | Ok(()) 88 | } 89 | } 90 | 91 | fn print_theme_install_info(theme: &Theme) { 92 | //print the friendly info about the installed theme 93 | println!( 94 | "{}{}{}{}{}{}", 95 | "Downloaded theme ".bright_blue().bold(), 96 | &theme.name.green(), 97 | ". \nTo set as default, use ".bright_blue().bold(), 98 | "leftwm-theme apply \"".bright_yellow().bold(), 99 | &theme.name.bright_yellow().bold(), 100 | "\"".bright_yellow().bold() 101 | ); 102 | } 103 | 104 | /// # Errors 105 | /// 106 | /// Will error if user does not return valid theme to ask or ask otherwise fails 107 | fn choose_one(themes: &mut [Theme]) -> Result<&mut Theme> { 108 | if themes.len() == 1 { 109 | Ok(&mut themes[0]) 110 | } else if themes.is_empty() { 111 | Err(friendly_message("No themes with that name were found")) 112 | } else { 113 | let idx = ask(themes)?; 114 | Ok(&mut themes[idx]) 115 | } 116 | } 117 | 118 | /// # Errors 119 | /// 120 | /// Should not error. 121 | fn ask(themes: &[Theme]) -> Result { 122 | #[allow(unused_assignments)] 123 | let mut return_index = Err(errors::LeftError::from("No themes available")); 124 | 'outer: loop { 125 | println!( 126 | "{}", 127 | "Which theme would you like to install?" 128 | .bright_yellow() 129 | .bold() 130 | ); 131 | for (id, theme) in themes.iter().enumerate() { 132 | if theme.directory.is_some() { 133 | error!("A theme with that name is already installed"); 134 | return_index = Err(errors::LeftError::from("Theme already installed")); 135 | break 'outer; 136 | } 137 | let source_string = match &theme.source { 138 | Some(source) => source.clone(), 139 | None => String::from("UNKNOWN"), 140 | }; 141 | println!( 142 | " {}/{} [{}]", 143 | &source_string.bright_magenta().bold(), 144 | &theme.name.bright_green().bold(), 145 | &id.to_string().bright_yellow().bold() 146 | ); 147 | } 148 | print!("{}", "=>".bright_yellow().bold()); 149 | io::stdout().flush().unwrap_or_default(); 150 | let val = read_num(); 151 | if let Ok(index) = val { 152 | if index < themes.len() { 153 | return_index = Ok(index); 154 | break; 155 | } 156 | } 157 | println!("{}", "Error: Please select a number:".bright_red().bold()); 158 | } 159 | return_index 160 | } 161 | 162 | /// # Errors 163 | /// 164 | /// Will error if unable to parse a number 165 | fn read_num() -> Result { 166 | let mut words = String::new(); 167 | io::stdin().read_line(&mut words).ok(); 168 | let trimmed = words.trim(); 169 | trace!("Trimmed receipt: {:?}", &trimmed); 170 | match trimmed.parse::() { 171 | Ok(size) => Ok(size), 172 | Err(err) => Err(errors::LeftError::from(err)), 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/operations/list.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::Config; 3 | use clap::Parser; 4 | use colored::Colorize; 5 | use log::trace; 6 | 7 | #[derive(Parser, Debug)] 8 | pub struct List { 9 | /// Names only 10 | #[clap(short = 'n', long)] 11 | pub names: bool, 12 | } 13 | 14 | impl List { 15 | /// # Errors 16 | /// 17 | /// Should not error. 18 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 19 | if !self.names { 20 | println!("{}", "\nInstalled themes:".blue().bold()); 21 | } 22 | let mut installed = 0; 23 | for repo in &config.repos { 24 | trace!("Printing themes from {}", &repo.name); 25 | for theme in &repo.themes { 26 | let current = match theme.current { 27 | Some(true) => "Current: ".bright_yellow().bold(), 28 | _ => "".white(), 29 | }; 30 | if theme.directory.is_some() && !self.names { 31 | println!( 32 | " {}{}/{}: {}", 33 | current, 34 | repo.name.bright_magenta().bold(), 35 | theme.name.bright_green().bold(), 36 | theme 37 | .description 38 | .as_ref() 39 | .unwrap_or(&"A LeftWM theme".to_string()) 40 | ); 41 | installed += 1; 42 | } else if theme.directory.is_some() && self.names { 43 | println!("{}", theme.name); 44 | installed += 1; 45 | } 46 | } 47 | } 48 | if installed == 0 { 49 | println!("{}", "No themes installed.".red().bold()); 50 | } 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/operations/migrate_toml_to_ron.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use leftwm_core::models::Gutter; 3 | use log::trace; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fs; 6 | use std::fs::File; 7 | use std::io::Write; 8 | use std::path::{Path, PathBuf}; 9 | 10 | use crate::errors::LeftError; 11 | 12 | /* Thes function converts a `theme.toml` provided by the `path` arg 13 | into a `theme.ron` at the same directory as the input file. 14 | Required argument is the path to the file that should be converted. 15 | */ 16 | 17 | #[derive(Debug, Parser)] 18 | pub struct Migrate { 19 | pub path: PathBuf, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 23 | struct Theme { 24 | border_width: i32, 25 | margin: CustomMargins, 26 | workspace_margin: Option, 27 | default_width: Option, 28 | default_height: Option, 29 | always_float: Option, 30 | gutter: Option>, 31 | default_border_color: String, 32 | floating_border_color: String, 33 | focused_border_color: String, 34 | #[serde(rename = "on_new_window")] 35 | on_new_window_cmd: Option, 36 | } 37 | 38 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 39 | #[serde(untagged)] 40 | enum CustomMargins { 41 | Int(u32), 42 | // format: [top, right, bottom, left] as per HTML 43 | Vec(Vec), 44 | } 45 | 46 | impl Migrate { 47 | /// # Errors 48 | /// 49 | /// Returns an error if theme file cannot be loaded / saved 50 | /// Returns an error if theme not found. 51 | pub fn exec(&self) -> Result<(), LeftError> { 52 | trace!("Migrating theme named {:?}", &self.path); 53 | match migrate(&self.path) { 54 | Ok(()) => Ok(()), 55 | Err(_) => Err(LeftError::from("Failed to migrate theme.")), 56 | } 57 | } 58 | } 59 | 60 | fn migrate(path: &PathBuf) -> Result<(), LeftError> { 61 | let Ok(theme) = load_theme_file(path) else { 62 | return Err(LeftError::from("Theme not found")); 63 | }; 64 | let mut ron_path = path.clone(); 65 | ron_path.set_extension("ron"); 66 | match write_to_file(&ron_path, &theme) { 67 | Ok(()) => Ok(()), 68 | Err(_) => Err(LeftError::from("Failed to write theme file.")), 69 | } 70 | } 71 | 72 | fn write_to_file(ron_file: &PathBuf, theme: &Theme) -> Result<(), LeftError> { 73 | let ron_pretty_conf = ron::ser::PrettyConfig::new() 74 | .depth_limit(2) 75 | .extensions(ron::extensions::Extensions::IMPLICIT_SOME); 76 | let ron_theme = ron::ser::to_string_pretty(&theme, ron_pretty_conf)?; 77 | let mut file = File::create(ron_file)?; 78 | file.write_all(ron_theme.as_bytes())?; 79 | Ok(()) 80 | } 81 | 82 | fn load_theme_file(path: impl AsRef) -> Result { 83 | let contents = fs::read_to_string(&path)?; 84 | let from_file: Theme = toml::from_str(&contents)?; 85 | Ok(from_file) 86 | } 87 | -------------------------------------------------------------------------------- /src/operations/mod.rs: -------------------------------------------------------------------------------- 1 | mod apply; 2 | //mod autofind; 3 | mod current; 4 | mod install; 5 | mod list; 6 | mod migrate_toml_to_ron; 7 | mod new; 8 | mod search; 9 | mod status; 10 | mod support; 11 | mod uninstall; 12 | mod update; 13 | mod upgrade; 14 | 15 | pub use apply::Apply; 16 | //pub use autofind::AutoFind; 17 | pub use current::Current; 18 | pub use install::Install; 19 | pub use list::List; 20 | pub use migrate_toml_to_ron::Migrate; 21 | pub use new::New; 22 | pub use search::Search; 23 | pub use status::Status; 24 | pub use support::Support; 25 | pub use uninstall::Uninstall; 26 | pub use update::Update; 27 | pub use upgrade::Upgrade; 28 | -------------------------------------------------------------------------------- /src/operations/new.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::{Config, Theme}; 3 | use crate::utils::read::one; 4 | use clap::Parser; 5 | use colored::Colorize; 6 | use git2::Repository; 7 | use log::{error, trace}; 8 | use regex::Regex; 9 | use std::io; 10 | use std::io::Write; 11 | use std::path::Path; 12 | use xdg::BaseDirectories; 13 | 14 | #[derive(Parser, Debug)] 15 | pub struct New { 16 | pub name: String, 17 | } 18 | 19 | // TODO: Allow themes with the same name in different namespaces 20 | impl New { 21 | /// # Errors 22 | /// 23 | /// Will send an error if theme has a `/`. 24 | /// Will error if a theme with same name already exists. 25 | /// Will error if config cannot be loaded or saved properly. 26 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 27 | New::validate_name(&self.name)?; 28 | 29 | if let Some(_theme) = Theme::find(config, &self.name) { 30 | error!( 31 | "\n{} could not be created because a theme with that name already exists", 32 | &self.name, 33 | ); 34 | Err(errors::LeftError::from("Theme not installed")) 35 | } else { 36 | //Create the new git in the leftwm directory 37 | let mut dir = BaseDirectories::with_prefix("leftwm")?.create_config_directory("")?; 38 | dir.push("themes"); 39 | dir.push(&self.name); 40 | match Repository::init(&dir) { 41 | Ok(_repo) => { 42 | Config::update_or_append( 43 | config, 44 | &Theme::new(&self.name, None, Some(dir.clone())), 45 | (&String::from("localhost"), &String::from("LOCAL")), 46 | ); 47 | Config::save(config)?; 48 | println!( 49 | "{} {} {} {}", 50 | "Theme".green().bold(), 51 | &self.name.red().bold(), 52 | "created successfully in".green().bold(), 53 | dir.to_str().unwrap_or("Unknown directory").red().bold() 54 | ); 55 | println!( 56 | "{}Which theme would you like to prefill?", 57 | "::".bright_yellow().bold() 58 | ); 59 | print!(" [0] basic_lemonbar\n [1] basic_polybar\n [2] basic_xmobar\n [3] None\n"); 60 | let state = loop { 61 | print!("{}", "0-3 =>".bright_yellow().bold()); 62 | io::stdout().flush().unwrap_or_default(); 63 | let state = one().trim().to_uppercase(); 64 | 65 | if state == *"0" || state == *"1" || state == *"2" || state == *"3" { 66 | break state; 67 | } 68 | 69 | println!("Please write a number 0-3."); 70 | }; 71 | match state.as_str() { 72 | "0" => copy_files("/usr/share/leftwm/themes/basic_lemonbar/", &dir), 73 | "1" => copy_files("/usr/share/leftwm/themes/basic_polybar/", &dir), 74 | "2" => copy_files("/usr/share/leftwm/themes/basic_xmobar/", &dir), 75 | _ => { 76 | trace!("Doing nothing"); 77 | Ok(()) 78 | } 79 | } 80 | } 81 | Err(e) => { 82 | error!( 83 | "\n{} could not be created because {:?}", 84 | &self.name, 85 | e.message() 86 | ); 87 | Err(errors::LeftError::from("Theme not created")) 88 | } 89 | } 90 | } 91 | } 92 | 93 | // Validates a given name for the theme name. 94 | fn validate_name(name: &str) -> Result { 95 | let mut valid: bool = true; 96 | 97 | // Should not contain '/'. 98 | if name.contains('/') { 99 | error!( 100 | "\n{} could not be created because a theme name should not contain '/'", 101 | &name, 102 | ); 103 | valid = false; 104 | } 105 | 106 | // Check for allowed characters. 107 | let re = Regex::new(r"^[a-z0-9_+-@.]*$").unwrap(); 108 | if valid && !re.is_match(name) { 109 | error!( 110 | "\n{} could not be created because a theme name can only contain lowercase alphanumeric characters and any of '@', '.', '_', '+', '-'", 111 | &name, 112 | ); 113 | valid = false; 114 | } 115 | 116 | // Should not have hyphens or dots at the beginning. 117 | let starts_with_re = Regex::new(r"^[-.]").unwrap(); 118 | if valid && starts_with_re.is_match(name) { 119 | error!( 120 | "\n{} could not be created because a theme name should not start with hyphens or dots", 121 | &name, 122 | ); 123 | valid = false; 124 | } 125 | 126 | if !valid { 127 | return Err(errors::LeftError::from("Theme name not valid.")); 128 | } 129 | 130 | Ok(true) 131 | } 132 | } 133 | 134 | fn copy_files(dir: &str, left_path: &Path) -> Result<(), errors::LeftError> { 135 | trace!("{:?}", &dir); 136 | let directory = Path::new(dir); 137 | trace!("{:?}", &directory); 138 | if directory.is_dir() { 139 | trace!("Directory Exists"); 140 | for entry in std::fs::read_dir(directory)? { 141 | trace!("{:?}", &entry); 142 | let entry = entry?; 143 | let path = entry.path(); 144 | trace!( 145 | "{:?}", 146 | std::fs::copy(path, left_path.join(entry.file_name())) 147 | ); 148 | } 149 | } else { 150 | error!("Basic themes directory /usr/share/leftwm/ not found. Was it installed by LeftWM?"); 151 | return Err(errors::LeftError::from("Theme not prefilled")); 152 | } 153 | Ok(()) 154 | } 155 | 156 | #[cfg(test)] 157 | mod test { 158 | use super::*; 159 | 160 | #[test] 161 | fn test_name_validation() { 162 | assert!(New::validate_name("test/theme").is_err()); 163 | assert!(New::validate_name("test theme").is_err()); 164 | assert!(New::validate_name("-testtheme").is_err()); 165 | assert!(New::validate_name(".testtheme").is_err()); 166 | assert!(New::validate_name("Testtheme").is_err()); 167 | 168 | assert!(New::validate_name("testtheme").is_ok()); 169 | assert!(New::validate_name("_testtheme").is_ok()); 170 | assert!(New::validate_name("1testtheme").is_ok()); 171 | assert!(New::validate_name("test1theme@").is_ok()); 172 | assert!(New::validate_name("test_theme").is_ok()); 173 | assert!(New::validate_name("test-theme").is_ok()); 174 | assert!(New::validate_name("test.theme").is_ok()); 175 | assert!(New::validate_name("test+theme").is_ok()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/operations/search.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::Config; 3 | use clap::Parser; 4 | use colored::Colorize; 5 | use errors::LeftError; 6 | use fuzzy_matcher::skim::SkimMatcherV2; 7 | use fuzzy_matcher::FuzzyMatcher; 8 | 9 | /* This function searches for themes, but does not update them by default 10 | * */ 11 | 12 | #[derive(Parser, Debug)] 13 | pub struct Search { 14 | /// Name of theme to find 15 | pub name: String, 16 | } 17 | 18 | impl Search { 19 | /// # Errors 20 | /// 21 | /// No errors expected. 22 | pub fn exec(&self, config: &mut Config) -> Result<(), LeftError> { 23 | // Load the configuration 24 | println!( 25 | "{}", 26 | "Searching for themes with similar names . . . " 27 | .bright_blue() 28 | .bold() 29 | ); 30 | // Iterate over the different themes, if the distance 31 | for theme in &config.themes(false) { 32 | if Search::fuzzy_matcher_match(&theme.name, &self.name) { 33 | let current = match theme.current { 34 | Some(true) => "Current: ".bright_yellow().bold(), 35 | _ => "".white(), 36 | }; 37 | let installed = match theme.directory { 38 | Some(_) => "-Installed".red().bold(), 39 | None => "".white(), 40 | }; 41 | println!( 42 | " {}{}/{}: {}{}", 43 | current, 44 | theme 45 | .source 46 | .clone() 47 | .unwrap_or_default() 48 | .bright_magenta() 49 | .bold(), 50 | theme.name.clone().bright_green().bold(), 51 | theme 52 | .description 53 | .as_ref() 54 | .unwrap_or(&"A LeftWM theme".to_string()), 55 | installed 56 | ); 57 | } 58 | } 59 | 60 | Ok(()) 61 | } 62 | 63 | // Performs a using fuzzy matcher. 64 | fn fuzzy_matcher_match(a: &str, b: &str) -> bool { 65 | let matcher = SkimMatcherV2::default(); 66 | matcher.fuzzy_match(a, b).is_some() 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod test { 72 | use super::*; 73 | 74 | #[test] 75 | fn test_match() { 76 | assert!(Search::fuzzy_matcher_match("apple pie", "apple")); 77 | assert!(Search::fuzzy_matcher_match("apple pie", "pie")); 78 | assert!(Search::fuzzy_matcher_match("Windows XP", "xp")); 79 | assert!(Search::fuzzy_matcher_match("Windows XP", "windows")); 80 | assert!(!Search::fuzzy_matcher_match("Windows XP", "zinbows")); 81 | assert!(Search::fuzzy_matcher_match("Soothe", "soo")); 82 | assert!(Search::fuzzy_matcher_match("Soothe", "soohe")); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/operations/status.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::{Config, LeftWm}; 3 | use clap::Parser; 4 | use colored::Colorize; 5 | 6 | #[derive(Parser, Debug)] 7 | pub struct Status { 8 | /// Error if not set 9 | #[clap(short = 'e', long)] 10 | pub error: bool, 11 | } 12 | 13 | impl Status { 14 | /// # Errors 15 | /// 16 | /// Will error if user flags -e AND no current theme is set in themes.toml. 17 | /// Will error if config cannot be loaded. 18 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 19 | println!( 20 | "{} {}", 21 | "Your LeftWM version is".bright_blue().bold(), 22 | LeftWm::get()?.version.bright_green().bold() 23 | ); 24 | let mut current = 0; 25 | let mut installed = 0; 26 | for theme in config.themes(false) { 27 | if theme.current == Some(true) { 28 | current += 1; 29 | println!( 30 | "{} {}, {} {} {}", 31 | "Your current theme is".bright_blue().bold(), 32 | theme.name.bright_green().bold(), 33 | "located in the".bright_blue().bold(), 34 | theme 35 | .source 36 | .unwrap_or_else(|| "unknown".to_string()) 37 | .bright_magenta() 38 | .bold(), 39 | "repo".bright_blue().bold() 40 | ); 41 | } 42 | if theme.directory.is_some() { 43 | installed += 1; 44 | } 45 | } 46 | println!( 47 | "{} {} {}", 48 | "There are".bright_blue().bold(), 49 | installed.to_string().bright_green().bold(), 50 | "themes installed in your ~/.config/leftwm/themes/ directory known to LeftWM." 51 | .bright_blue() 52 | .bold() 53 | ); 54 | if current == 0 { 55 | println!( 56 | "{} \n {}", 57 | "WARNING! NO KNOWN THEME IS CURRENTLY SET." 58 | .bright_red() 59 | .bold(), 60 | "A theme may be set, but LeftWM theme doesn't know about it.\n If it is a local theme, try leftwm-theme new themename.\n If it is a repo theme, try leftwm-theme install themename" 61 | .bright_yellow() 62 | .bold() 63 | ); 64 | if self.error { 65 | return Err(errors::friendly_message("Error! No theme set.")); 66 | } 67 | } 68 | Ok(()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/operations/support.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::Config; 3 | use clap::Parser; 4 | use log::trace; 5 | use std::process::Command; 6 | 7 | #[derive(Parser, Debug)] 8 | pub struct Support { 9 | /// Names 10 | pub name: String, 11 | } 12 | 13 | impl Support { 14 | /// # Errors 15 | /// 16 | /// Should not error. 17 | /// 18 | /// # Panics 19 | /// May panic if xdg-open not found, or handler not set 20 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 21 | 'outer: for repo in &config.repos { 22 | trace!("Searching themes from {}", &repo.name); 23 | for theme in &repo.themes { 24 | if theme.name == self.name { 25 | if let Some(s_url) = &theme.support_url { 26 | Command::new("xdg-open") 27 | .arg(s_url) 28 | .spawn() 29 | .expect("Could not xdg-open"); 30 | } else { 31 | println!("Theme does not have associated help page."); 32 | } 33 | break 'outer; 34 | } 35 | } 36 | } 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/operations/uninstall.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::errors::LeftError; 3 | use crate::models::{Config, Theme}; 4 | use crate::utils::read::yes_or_no; 5 | use clap::Parser; 6 | use colored::Colorize; 7 | use log::error; 8 | use std::fs; 9 | use std::path::Path; 10 | 11 | #[derive(Parser, Debug)] 12 | pub struct Uninstall { 13 | /// Name of theme to uninstall 14 | pub name: String, 15 | /// Whether to prompt for confirmation 16 | #[clap(long)] 17 | pub noconfirm: bool, 18 | } 19 | 20 | impl Uninstall { 21 | /// # Errors 22 | /// Will error if config cannot be saved. 23 | /// Will error if cannot remove directory. 24 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 25 | println!( 26 | "{}", 27 | "Looking for theme to uninstall . . . ".bright_blue().bold() 28 | ); 29 | let Some(theme) = Theme::find_installed(config, &self.name) else { 30 | return Err(LeftError::from("Theme not found")); 31 | }; 32 | if let Some(directory) = theme.directory { 33 | let path = Path::new(&directory); 34 | if self.noconfirm 35 | || yes_or_no(&format!( 36 | " Are you sure you want to uninstall this theme, located at {}?", 37 | path.to_str().unwrap_or("Unknown location") 38 | )) 39 | { 40 | fs::remove_dir_all(path)?; 41 | match theme.source { 42 | Some(source) => match Theme::find_mut(config, &self.name, &source) { 43 | Some(target_theme) => { 44 | target_theme.directory = None; 45 | println!( 46 | "{}", 47 | format!("Theme {} uninstalled.", &self.name).green().bold() 48 | ); 49 | } 50 | None => return Err(LeftError::from("Could not find theme")), 51 | }, 52 | None => return Err(LeftError::from("No source found")), 53 | } 54 | Config::save(config)?; 55 | } else { 56 | println!("{}", "No actions to take. Exiting . . . ".yellow().bold()); 57 | } 58 | 59 | Ok(()) 60 | } else { 61 | error!("Theme not installed"); 62 | Err(errors::LeftError::from("Theme not installed")) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/operations/update.rs: -------------------------------------------------------------------------------- 1 | use crate::models::Config; 2 | use crate::{errors, utils}; 3 | use clap::Parser; 4 | use colored::Colorize; 5 | use log::trace; 6 | use std::fs; 7 | use url::Url; 8 | 9 | #[derive(Parser, Debug)] 10 | pub struct Update { 11 | /// Don't list themes 12 | #[clap(short = 'f', long)] 13 | pub no_list: bool, 14 | /// List incompatible themes 15 | #[clap(short = 'i', long)] 16 | pub incompatible: bool, 17 | } 18 | 19 | impl Update { 20 | /// Fetch themes from the themes repository. 21 | /// 22 | /// # Errors 23 | /// 24 | /// Will error if config cannot be saved 25 | /// Will error if upstream known.toml cannot be retrieved. 26 | /// Will error if TOML files themes.toml or known.toml cannot be parsed. 27 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 28 | Update::update_repos(config)?; 29 | Config::save(config)?; 30 | 31 | // Exit early if --no-list was passed 32 | if self.no_list { 33 | return Ok(()); 34 | } 35 | 36 | // List themes 37 | println!("{}", "\nAvailable themes:".bright_blue().bold()); 38 | 39 | for repo in &mut config.repos { 40 | for theme in &mut repo.themes { 41 | let current = match theme.current { 42 | Some(true) => "Current: ".bright_green().bold(), 43 | _ => "".white(), 44 | }; 45 | let installed = match theme.directory { 46 | Some(_) => "-Installed".red().bold(), 47 | None => "".white(), 48 | }; 49 | // Only list installable themes 50 | if let Ok(true) = utils::versions::check( 51 | &theme 52 | .leftwm_versions 53 | .clone() 54 | .unwrap_or_else(|| "*".to_string()), 55 | ) { 56 | println!( 57 | " {}{}/{}: {}{}", 58 | current, 59 | repo.name.bright_magenta().bold(), 60 | theme.name.bright_green().bold(), 61 | theme 62 | .description 63 | .as_ref() 64 | .unwrap_or(&"A LeftWM theme".to_string()), 65 | installed 66 | ); 67 | } else { 68 | // Show incompatible themes if requested 69 | if self.incompatible { 70 | println!( 71 | " {}{}/{}: {}{} (not compatible with your version of leftwm)", 72 | current, 73 | repo.name.bright_magenta().bold(), 74 | theme.name.bright_red().bold(), 75 | theme 76 | .description 77 | .as_ref() 78 | .unwrap_or(&"A LeftWM theme".to_string()), 79 | installed 80 | ); 81 | } 82 | } 83 | } 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | // Iterates through the repos in the config, fetches themes from the repos 90 | // and updates the config with the themes. The downloaded themes are 91 | // compared with any existing themes and updated. 92 | fn update_repos(config: &mut Config) -> Result<(), errors::LeftError> { 93 | println!("{}", "Fetching themes . . . ".bright_blue().bold()); 94 | let config_dir = config.get_config_dir()?; 95 | // Attempt to fetch new themes and populate the config with remote 96 | // themes. 97 | trace!("{:?}", &config); 98 | for repo in &mut config.repos { 99 | if repo.name == "LOCAL" { 100 | // Update local repos separately after processing the remote 101 | // repos. 102 | continue; 103 | } 104 | 105 | let content: String; 106 | // Check the url scheme to determine how to fetch the themes. 107 | let repo_url = Url::parse(repo.url.clone().as_str())?; 108 | if repo_url.scheme() == "file" { 109 | content = fs::read_to_string(repo_url.path())?; 110 | } else { 111 | content = reqwest::blocking::get(repo_url.as_str())?.text_with_charset("utf-8")?; 112 | trace!("{:?}", &content); 113 | } 114 | if !content.is_empty() { 115 | repo.compare(toml::from_str(&content)?, &config_dir)?; 116 | } 117 | } 118 | 119 | // Populate config based on the local themes. 120 | config.update_local_repo()?; 121 | 122 | Ok(()) 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod test { 128 | use super::*; 129 | use crate::models::Repo; 130 | use std::fs::File; 131 | use std::io::Write; 132 | 133 | #[test] 134 | fn test_update_repos() { 135 | // Create a local repo file with test themes. 136 | let tmpdir = tempfile::tempdir().unwrap(); 137 | let repo_file_path = tmpdir.path().join("repo.toml"); 138 | let mut repo_file = File::create(&repo_file_path).unwrap(); 139 | write!( 140 | repo_file, 141 | r#" 142 | [[theme]] 143 | name = "test-theme1" 144 | repository = "https://github.com/leftwm/testtheme1/" 145 | commit = "*" 146 | version = "0.0.5" 147 | leftwm_versions = "*" 148 | 149 | [[theme]] 150 | name = "test-theme2" 151 | repository = "https://github.com/leftwm/testtheme2/" 152 | commit = "*" 153 | version = "0.0.3" 154 | leftwm_versions = "*" 155 | "# 156 | ) 157 | .unwrap(); 158 | let url_base = Url::parse("file://").unwrap(); 159 | let local_file_url = url_base.join(repo_file_path.to_str().unwrap()).unwrap(); 160 | 161 | let mut config: Config = Config { 162 | repos: vec![ 163 | Repo { 164 | definitions_version: crate::models::config::CURRENT_DEFINITIONS_VERSION, 165 | url: String::from(local_file_url.as_str()), 166 | name: String::from("test-repo"), 167 | themes: Vec::new(), 168 | }, 169 | Repo{ 170 | definitions_version: crate::models::config::CURRENT_DEFINITIONS_VERSION, 171 | url: String::from("https://raw.githubusercontent.com/leftwm/leftwm-community-themes/master/known.toml"), 172 | name: String::from("community"), 173 | themes: Vec::new(), 174 | }, 175 | ], 176 | config_dir: Some(tmpdir.path().to_path_buf()), 177 | }; 178 | 179 | assert!(Update::update_repos(&mut config).is_ok()); 180 | let local_repo = config 181 | .repos 182 | .into_iter() 183 | .find(|x| x.name == "test-repo") 184 | .unwrap(); 185 | assert_eq!(local_repo.themes.len(), 2); 186 | assert!(local_repo 187 | .clone() 188 | .themes 189 | .into_iter() 190 | .any(|x| x.name == "test-theme1")); 191 | assert!(local_repo 192 | .themes 193 | .into_iter() 194 | .any(|x| x.name == "test-theme2")); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/operations/upgrade.rs: -------------------------------------------------------------------------------- 1 | //! Updates each individual theme. 2 | // Currently, there is no way of knowing whether a theme needs updated. In a later version it would 3 | // be nice to skip themes that do not need updates. 4 | use crate::errors; 5 | use crate::models::Config; 6 | use clap::Parser; 7 | use colored::Colorize; 8 | use git2::{Oid, Repository}; 9 | use log::{error, trace}; 10 | 11 | #[derive(Parser, Debug)] 12 | pub struct Upgrade { 13 | /// Don't update db 14 | #[clap(short = 'i', long)] 15 | pub skipdbupdate: bool, 16 | } 17 | 18 | impl Upgrade { 19 | /// # Errors 20 | /// 21 | /// This function will return an error if the known.toml file fails to load correctly, or if 22 | /// comparisons fail (i.e. the TOML files do not parse), or if the config file cannot be saved. 23 | /// It will also throw an error if a config file does not have a proper directory 24 | /// It will not throw an error to the program that a particular theme repository failed to 25 | /// load, instead passing that information to the user. 26 | /// 27 | ///# Panics 28 | /// 29 | /// Panics are not expected. `theme.commit.as_ref().unwrap()` is within an if `is_some()` 30 | // Todo: allow passage of failed themes in either () or errors::LeftError 31 | pub fn exec(&self, config: &mut Config) -> Result<(), errors::LeftError> { 32 | //attempt to fetch new themes 33 | if !self.skipdbupdate { 34 | println!("{}", "Fetching known themes:".bright_blue().bold()); 35 | let config_dir = config.get_config_dir()?; 36 | for repo in &mut config.repos { 37 | if repo.name == "LOCAL" { 38 | continue; 39 | } 40 | println!( 41 | " Retrieving themes from {}", 42 | &repo.name.bright_magenta().bold() 43 | ); 44 | // We probably ought to add a better warning here if this fails to load 45 | let resp = reqwest::blocking::get(&repo.url)?.text_with_charset("utf-8")?; 46 | trace!("{:?}", &resp); 47 | 48 | //compare to old themes 49 | repo.compare(toml::from_str(&resp)?, &config_dir)?; 50 | } 51 | Config::save(config)?; 52 | } 53 | // Update themes 54 | println!("{}", "\nUpdating themes:".bright_blue().bold()); 55 | let mut installed = 0; 56 | for repo in &config.repos { 57 | trace!("Upgrading themes in repo {:?}", &repo.name); 58 | if repo.name == "LOCAL" { 59 | continue; 60 | } 61 | for theme in &repo.themes { 62 | let current = match theme.current { 63 | Some(true) => "Current: ".bright_green().bold(), 64 | _ => "".white(), 65 | }; 66 | if let Some(theme_directory) = &theme.directory { 67 | println!( 68 | " Updating {}{}/{}: {}", 69 | current, 70 | repo.name.bright_magenta().bold(), 71 | theme.name.bright_yellow().bold(), 72 | theme 73 | .description 74 | .as_ref() 75 | .unwrap_or(&"A LeftWM theme".to_string()) 76 | ); 77 | let git_repo = Repository::open(theme_directory)?; 78 | match fetch_origin_main(&git_repo) { 79 | Ok(()) => { 80 | //if defined, attempt to checkout the specific index 81 | if theme.commit.is_some() 82 | && theme.commit.clone().unwrap_or_default() != *"*" 83 | { 84 | git_repo.set_head_detached(Oid::from_str( 85 | theme.commit.as_ref().unwrap(), 86 | )?)?; 87 | git_repo.checkout_head(None)?; 88 | } 89 | } 90 | Err(e) => { 91 | trace!("Error: {:?}", e); 92 | error!("Could not fetch repo."); 93 | } 94 | } 95 | 96 | installed += 1; 97 | } 98 | } 99 | } 100 | if installed == 0 { 101 | println!("{}", "No themes installed.".red().bold()); 102 | } 103 | Ok(()) 104 | } 105 | } 106 | 107 | pub(crate) fn fetch_origin_main(repo: &git2::Repository) -> Result<(), git2::Error> { 108 | use crate::utils::merge::{run, Args}; 109 | let args = Args { 110 | arg_remote: None, 111 | arg_branch: None, 112 | }; 113 | run(&args, repo) 114 | } 115 | -------------------------------------------------------------------------------- /src/utils/dir.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Result; 2 | use xdg::BaseDirectories; 3 | /// # Errors 4 | /// 5 | /// Will error if `BaseDirectory` not set 6 | /// Will error if unable to create theme leftwm directory 7 | pub fn theme() -> Result { 8 | let mut dir = BaseDirectories::with_prefix("leftwm")?.create_config_directory("")?; 9 | dir.push("themes"); 10 | Ok(dir) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/merge.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * libgit2 "pull" example - shows how to pull remote data into a local branch. 3 | * 4 | * Written by the libgit2 contributors 5 | * 6 | * To the extent possible under law, the author(s) have dedicated all copyright 7 | * and related and neighboring rights to this software to the public domain 8 | * worldwide. This software is distributed without any warranty. 9 | * 10 | * You should have received a copy of the CC0 Public Domain Dedication along 11 | * with this software. If not, see 12 | * . 13 | */ 14 | 15 | use colored::Colorize; 16 | use git2::Repository; 17 | use log::trace; 18 | use std::io::{self, Write}; 19 | use std::str; 20 | 21 | pub struct Args { 22 | pub arg_remote: Option, 23 | pub arg_branch: Option, 24 | } 25 | 26 | fn do_fetch<'a>( 27 | repo: &'a git2::Repository, 28 | refs: &[&str], 29 | remote: &'a mut git2::Remote, 30 | ) -> Result, git2::Error> { 31 | let mut cb = git2::RemoteCallbacks::new(); 32 | 33 | // Print out our transfer progress. 34 | cb.transfer_progress(|stats| { 35 | if stats.received_objects() == stats.total_objects() { 36 | print!( 37 | " Resolving deltas {}/{}\r", 38 | stats.indexed_deltas(), 39 | stats.total_deltas() 40 | ); 41 | } else if stats.total_objects() > 0 { 42 | print!( 43 | " Received {}/{} objects ({}) in {} bytes\r", 44 | stats.received_objects(), 45 | stats.total_objects(), 46 | stats.indexed_objects(), 47 | stats.received_bytes() 48 | ); 49 | } 50 | io::stdout().flush().unwrap(); 51 | true 52 | }); 53 | 54 | let mut fo = git2::FetchOptions::new(); 55 | fo.remote_callbacks(cb); 56 | // Always fetch all tags. 57 | // Perform a download and also update tips 58 | fo.download_tags(git2::AutotagOption::All); 59 | trace!("Fetching {} for repo", remote.name().unwrap()); 60 | remote.fetch(refs, Some(&mut fo), None)?; 61 | 62 | // If there are local objects (we got a thin pack), then tell the user 63 | // how many objects we saved from having to cross the network. 64 | let stats = remote.stats(); 65 | if stats.local_objects() > 0 { 66 | println!( 67 | "\r Received {}/{} objects in {} bytes (used {} local \ 68 | objects)", 69 | stats.indexed_objects(), 70 | stats.total_objects(), 71 | stats.received_bytes(), 72 | stats.local_objects() 73 | ); 74 | } else { 75 | println!( 76 | "\r Received {}/{} objects in {} bytes", 77 | stats.indexed_objects(), 78 | stats.total_objects(), 79 | stats.received_bytes() 80 | ); 81 | } 82 | 83 | let fetch_head = repo.find_reference("FETCH_HEAD")?; 84 | repo.reference_to_annotated_commit(&fetch_head) 85 | } 86 | 87 | fn fast_forward( 88 | repo: &Repository, 89 | lb: &mut git2::Reference, 90 | rc: &git2::AnnotatedCommit, 91 | ) -> Result<(), git2::Error> { 92 | let name = match lb.name() { 93 | Some(s) => s.to_string(), 94 | None => String::from_utf8_lossy(lb.name_bytes()).to_string(), 95 | }; 96 | let msg = format!(" Fast-Forward: Setting {name} to id: {}", rc.id()); 97 | println!("{msg}"); 98 | lb.set_target(rc.id(), &msg)?; 99 | repo.set_head(&name)?; 100 | repo.checkout_head(Some( 101 | git2::build::CheckoutBuilder::default() 102 | // For some reason the force is required to make the working directory actually get updated 103 | // I suspect we should be adding some logic to handle dirty working directory states 104 | // but this is just an example so maybe not. 105 | .force(), 106 | ))?; 107 | println!("{}", " ->OK".bright_green().bold()); 108 | Ok(()) 109 | } 110 | 111 | fn normal_merge( 112 | repo: &Repository, 113 | local: &git2::AnnotatedCommit, 114 | remote: &git2::AnnotatedCommit, 115 | ) -> Result<(), git2::Error> { 116 | let local_tree = repo.find_commit(local.id())?.tree()?; 117 | let remote_tree = repo.find_commit(remote.id())?.tree()?; 118 | let ancestor = repo 119 | .find_commit(repo.merge_base(local.id(), remote.id())?)? 120 | .tree()?; 121 | let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?; 122 | 123 | if idx.has_conflicts() { 124 | println!( 125 | "{}", 126 | " ->Merge conficts detected, you will need to fix these!" 127 | .bright_red() 128 | .bold() 129 | ); 130 | repo.checkout_index(Some(&mut idx), None)?; 131 | return Ok(()); 132 | } 133 | let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?; 134 | // now create the merge commit 135 | let msg = format!("Merge: {} into {}", remote.id(), local.id()); 136 | let sig = repo.signature()?; 137 | let local_commit = repo.find_commit(local.id())?; 138 | let remote_commit = repo.find_commit(remote.id())?; 139 | // Do our merge commit and set current branch head to that commit. 140 | let _merge_commit = repo.commit( 141 | Some("HEAD"), 142 | &sig, 143 | &sig, 144 | &msg, 145 | &result_tree, 146 | &[&local_commit, &remote_commit], 147 | )?; 148 | // Set working tree to match head. 149 | repo.checkout_head(None)?; 150 | println!("{}", " ->OK".bright_green().bold()); 151 | Ok(()) 152 | } 153 | 154 | fn do_merge<'a>( 155 | repo: &'a Repository, 156 | remote_branch: &str, 157 | fetch_commit: &git2::AnnotatedCommit<'a>, 158 | ) -> Result<(), git2::Error> { 159 | // 1. do a merge analysis 160 | let analysis = repo.merge_analysis(&[fetch_commit])?; 161 | 162 | // 2. Do the appopriate merge 163 | if analysis.0.is_fast_forward() { 164 | trace!("Doing a fast forward"); 165 | // do a fast forward 166 | let refname = format!("refs/heads/{remote_branch}"); 167 | if let Ok(mut r) = repo.find_reference(&refname) { 168 | fast_forward(repo, &mut r, fetch_commit)?; 169 | } else { 170 | // The branch doesn't exist so just set the reference to the 171 | // commit directly. Usually this is because you are pulling 172 | // into an empty repository. 173 | repo.reference( 174 | &refname, 175 | fetch_commit.id(), 176 | true, 177 | &format!(" Setting {remote_branch} to {}", fetch_commit.id()), 178 | )?; 179 | repo.set_head(&refname)?; 180 | repo.checkout_head(Some( 181 | git2::build::CheckoutBuilder::default() 182 | .allow_conflicts(true) 183 | .conflict_style_merge(true) 184 | .force(), 185 | ))?; 186 | } 187 | } else if analysis.0.is_normal() { 188 | // do a normal merge 189 | let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?; 190 | normal_merge(repo, &head_commit, fetch_commit)?; 191 | } else { 192 | println!("{}", " ->OK".bright_green().bold()); 193 | trace!("Nothing to do..."); 194 | } 195 | Ok(()) 196 | } 197 | 198 | /// Run git-pull and use optimal strategy 199 | /// 200 | /// # Errors 201 | /// - See `git2` errors. 202 | pub fn run(args: &Args, repo: &Repository) -> Result<(), git2::Error> { 203 | let remote_name = args.arg_remote.as_ref().map_or("origin", |s| &s[..]); 204 | let remote_branch = args.arg_branch.as_ref().map_or("master", |s| &s[..]); 205 | let mut remote = repo.find_remote(remote_name)?; 206 | let fetch_commit = do_fetch(repo, &[remote_branch], &mut remote)?; 207 | do_merge(repo, remote_branch, &fetch_commit) 208 | } 209 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dir; 2 | pub mod merge; 3 | pub mod read; 4 | pub mod versions; 5 | -------------------------------------------------------------------------------- /src/utils/read.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use std::io; 3 | use std::io::Write; 4 | 5 | #[must_use] 6 | pub fn one() -> String { 7 | let mut words = String::new(); 8 | io::stdin().read_line(&mut words).ok(); 9 | words 10 | } 11 | 12 | #[must_use] 13 | pub fn yes_or_no(question: &str) -> bool { 14 | let state = loop { 15 | println!(" {question}"); 16 | print!("{}", "yes/no =>".bright_yellow().bold()); 17 | io::stdout().flush().unwrap_or_default(); 18 | let state = one().trim().to_uppercase(); 19 | 20 | if state == *"YES" || state == *"NO" { 21 | break state; 22 | } 23 | 24 | println!("Please write either yes or no."); 25 | }; 26 | matches!(state.as_str(), "YES") 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/versions.rs: -------------------------------------------------------------------------------- 1 | use crate::errors; 2 | use crate::models::LeftWm; 3 | /// # Errors 4 | /// 5 | /// Returns error if the `LeftWM` version cannot be obtained. 6 | /// Returns error if the `LeftWM` version requirements cannot be parsed. 7 | /// Returns error is the `LeftWM` version cannot be parsed. 8 | pub fn check(vstring: &str) -> Result { 9 | use semver::{Version, VersionReq}; 10 | let lwmv = LeftWm::get()?; 11 | let requirements = VersionReq::parse(vstring)?; 12 | Ok(requirements.matches(&Version::parse(&lwmv.version)?)) 13 | } 14 | --------------------------------------------------------------------------------