├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── LICENSE ├── README.md ├── build.rs ├── clippy.toml ├── docs └── YAML.jpg ├── rustfmt.toml └── src ├── cmd ├── apply.rs ├── dump.rs └── mod.rs ├── defaults.rs ├── errors.rs └── main.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/ 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with cargo-dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (cargo-dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'cargo dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-20.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Install cargo-dist 63 | # we specify bash to get pipefail; it guards against the `curl` command 64 | # failing. otherwise `sh` won't catch that `curl` returned non-0 65 | shell: bash 66 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh" 67 | - name: Cache cargo-dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/cargo-dist 72 | # sure would be cool if github gave us proper conditionals... 73 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 74 | # functionality based on whether this is a pull_request, and whether it's from a fork. 75 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 76 | # but also really annoying to build CI around when it needs secrets to work right.) 77 | - id: plan 78 | run: | 79 | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "cargo dist ran successfully" 81 | cat plan-dist-manifest.json 82 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 83 | - name: "Upload dist-manifest.json" 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: artifacts-plan-dist-manifest 87 | path: plan-dist-manifest.json 88 | 89 | # Build and packages all the platform-specific things 90 | build-local-artifacts: 91 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 92 | # Let the initial task tell us to not run (currently very blunt) 93 | needs: 94 | - plan 95 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 96 | strategy: 97 | fail-fast: false 98 | # Target platforms/runners are computed by cargo-dist in create-release. 99 | # Each member of the matrix has the following arguments: 100 | # 101 | # - runner: the github runner 102 | # - dist-args: cli flags to pass to cargo dist 103 | # - install-dist: expression to run to install cargo-dist on the runner 104 | # 105 | # Typically there will be: 106 | # - 1 "global" task that builds universal installers 107 | # - N "local" tasks that build each platform's binaries and platform-specific installers 108 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 109 | runs-on: ${{ matrix.runner }} 110 | env: 111 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 113 | steps: 114 | - name: enable windows longpaths 115 | run: | 116 | git config --global core.longpaths true 117 | - uses: actions/checkout@v4 118 | with: 119 | submodules: recursive 120 | - name: Install cargo-dist 121 | run: ${{ matrix.install_dist }} 122 | # Get the dist-manifest 123 | - name: Fetch local artifacts 124 | uses: actions/download-artifact@v4 125 | with: 126 | pattern: artifacts-* 127 | path: target/distrib/ 128 | merge-multiple: true 129 | - name: Install dependencies 130 | run: | 131 | ${{ matrix.packages_install }} 132 | - name: Build artifacts 133 | run: | 134 | # Actually do builds and make zips and whatnot 135 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 136 | echo "cargo dist ran successfully" 137 | - id: cargo-dist 138 | name: Post-build 139 | # We force bash here just because github makes it really hard to get values up 140 | # to "real" actions without writing to env-vars, and writing to env-vars has 141 | # inconsistent syntax between shell and powershell. 142 | shell: bash 143 | run: | 144 | # Parse out what we just built and upload it to scratch storage 145 | echo "paths<> "$GITHUB_OUTPUT" 146 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 147 | echo "EOF" >> "$GITHUB_OUTPUT" 148 | 149 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 150 | - name: "Upload artifacts" 151 | uses: actions/upload-artifact@v4 152 | with: 153 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 154 | path: | 155 | ${{ steps.cargo-dist.outputs.paths }} 156 | ${{ env.BUILD_MANIFEST_NAME }} 157 | 158 | # Build and package all the platform-agnostic(ish) things 159 | build-global-artifacts: 160 | needs: 161 | - plan 162 | - build-local-artifacts 163 | runs-on: "ubuntu-20.04" 164 | env: 165 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 166 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 167 | steps: 168 | - uses: actions/checkout@v4 169 | with: 170 | submodules: recursive 171 | - name: Install cached cargo-dist 172 | uses: actions/download-artifact@v4 173 | with: 174 | name: cargo-dist-cache 175 | path: ~/.cargo/bin/ 176 | - run: chmod +x ~/.cargo/bin/cargo-dist 177 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 178 | - name: Fetch local artifacts 179 | uses: actions/download-artifact@v4 180 | with: 181 | pattern: artifacts-* 182 | path: target/distrib/ 183 | merge-multiple: true 184 | - id: cargo-dist 185 | shell: bash 186 | run: | 187 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 188 | echo "cargo dist ran successfully" 189 | 190 | # Parse out what we just built and upload it to scratch storage 191 | echo "paths<> "$GITHUB_OUTPUT" 192 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 193 | echo "EOF" >> "$GITHUB_OUTPUT" 194 | 195 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 196 | - name: "Upload artifacts" 197 | uses: actions/upload-artifact@v4 198 | with: 199 | name: artifacts-build-global 200 | path: | 201 | ${{ steps.cargo-dist.outputs.paths }} 202 | ${{ env.BUILD_MANIFEST_NAME }} 203 | # Determines if we should publish/announce 204 | host: 205 | needs: 206 | - plan 207 | - build-local-artifacts 208 | - build-global-artifacts 209 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 210 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 211 | env: 212 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 213 | runs-on: "ubuntu-20.04" 214 | outputs: 215 | val: ${{ steps.host.outputs.manifest }} 216 | steps: 217 | - uses: actions/checkout@v4 218 | with: 219 | submodules: recursive 220 | - name: Install cached cargo-dist 221 | uses: actions/download-artifact@v4 222 | with: 223 | name: cargo-dist-cache 224 | path: ~/.cargo/bin/ 225 | - run: chmod +x ~/.cargo/bin/cargo-dist 226 | # Fetch artifacts from scratch-storage 227 | - name: Fetch artifacts 228 | uses: actions/download-artifact@v4 229 | with: 230 | pattern: artifacts-* 231 | path: target/distrib/ 232 | merge-multiple: true 233 | - id: host 234 | shell: bash 235 | run: | 236 | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 237 | echo "artifacts uploaded and released successfully" 238 | cat dist-manifest.json 239 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 240 | - name: "Upload dist-manifest.json" 241 | uses: actions/upload-artifact@v4 242 | with: 243 | # Overwrite the previous copy 244 | name: artifacts-dist-manifest 245 | path: dist-manifest.json 246 | # Create a GitHub Release while uploading all files to it 247 | - name: "Download GitHub Artifacts" 248 | uses: actions/download-artifact@v4 249 | with: 250 | pattern: artifacts-* 251 | path: artifacts 252 | merge-multiple: true 253 | - name: Cleanup 254 | run: | 255 | # Remove the granular manifests 256 | rm -f artifacts/*-dist-manifest.json 257 | - name: Create GitHub Release 258 | env: 259 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 260 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 261 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 262 | RELEASE_COMMIT: "${{ github.sha }}" 263 | run: | 264 | # Write and read notes from a file to avoid quoting breaking things 265 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 266 | 267 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 268 | 269 | publish-homebrew-formula: 270 | needs: 271 | - plan 272 | - host 273 | runs-on: "ubuntu-20.04" 274 | env: 275 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 276 | PLAN: ${{ needs.plan.outputs.val }} 277 | GITHUB_USER: "axo bot" 278 | GITHUB_EMAIL: "admin+bot@axo.dev" 279 | if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} 280 | steps: 281 | - uses: actions/checkout@v4 282 | with: 283 | repository: "dsully/homebrew-tap" 284 | token: ${{ secrets.HOMEBREW_TAP_TOKEN }} 285 | # So we have access to the formula 286 | - name: Fetch homebrew formulae 287 | uses: actions/download-artifact@v4 288 | with: 289 | pattern: artifacts-* 290 | path: Formula/ 291 | merge-multiple: true 292 | # This is extra complex because you can make your Formula name not match your app name 293 | # so we need to find releases with a *.rb file, and publish with that filename. 294 | - name: Commit formula files 295 | run: | 296 | git config --global user.name "${GITHUB_USER}" 297 | git config --global user.email "${GITHUB_EMAIL}" 298 | 299 | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do 300 | filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) 301 | name=$(echo "$filename" | sed "s/\.rb$//") 302 | version=$(echo "$release" | jq .app_version --raw-output) 303 | 304 | export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" 305 | brew update 306 | # We avoid reformatting user-provided data such as the app description and homepage. 307 | brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true 308 | 309 | git add "Formula/${filename}" 310 | git commit -m "${name} ${version}" 311 | done 312 | git push 313 | 314 | announce: 315 | needs: 316 | - plan 317 | - host 318 | - publish-homebrew-formula 319 | # use "always() && ..." to allow us to wait for all publish jobs while 320 | # still allowing individual publish jobs to skip themselves (for prereleases). 321 | # "host" however must run to completion, no skipping allowed! 322 | if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }} 323 | runs-on: "ubuntu-20.04" 324 | env: 325 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 326 | steps: 327 | - uses: actions/checkout@v4 328 | with: 329 | submodules: recursive 330 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /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.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.15" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.8" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.5" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 64 | dependencies = [ 65 | "windows-sys 0.52.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.4" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.52.0", 76 | ] 77 | 78 | [[package]] 79 | name = "backtrace" 80 | version = "0.3.71" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 83 | dependencies = [ 84 | "addr2line", 85 | "cc", 86 | "cfg-if", 87 | "libc", 88 | "miniz_oxide", 89 | "object", 90 | "rustc-demangle", 91 | ] 92 | 93 | [[package]] 94 | name = "base64" 95 | version = "0.22.1" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 98 | 99 | [[package]] 100 | name = "bitflags" 101 | version = "2.6.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 104 | 105 | [[package]] 106 | name = "camino" 107 | version = "1.1.9" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 110 | 111 | [[package]] 112 | name = "cc" 113 | version = "1.1.21" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" 116 | dependencies = [ 117 | "shlex", 118 | ] 119 | 120 | [[package]] 121 | name = "cfg-if" 122 | version = "1.0.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 125 | 126 | [[package]] 127 | name = "clap" 128 | version = "4.5.17" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" 131 | dependencies = [ 132 | "clap_builder", 133 | "clap_derive", 134 | ] 135 | 136 | [[package]] 137 | name = "clap-verbosity-flag" 138 | version = "2.2.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "63d19864d6b68464c59f7162c9914a0b569ddc2926b4a2d71afe62a9738eff53" 141 | dependencies = [ 142 | "clap", 143 | "log", 144 | ] 145 | 146 | [[package]] 147 | name = "clap_builder" 148 | version = "4.5.17" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" 151 | dependencies = [ 152 | "anstream", 153 | "anstyle", 154 | "clap_lex", 155 | "strsim", 156 | "terminal_size", 157 | ] 158 | 159 | [[package]] 160 | name = "clap_complete" 161 | version = "4.5.28" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "9b378c786d3bde9442d2c6dd7e6080b2a818db2b96e30d6e7f1b6d224eb617d3" 164 | dependencies = [ 165 | "clap", 166 | ] 167 | 168 | [[package]] 169 | name = "clap_derive" 170 | version = "4.5.13" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 173 | dependencies = [ 174 | "heck", 175 | "proc-macro2", 176 | "quote", 177 | "syn", 178 | ] 179 | 180 | [[package]] 181 | name = "clap_lex" 182 | version = "0.7.2" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 185 | 186 | [[package]] 187 | name = "color-eyre" 188 | version = "0.6.3" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" 191 | dependencies = [ 192 | "backtrace", 193 | "color-spantrace", 194 | "eyre", 195 | "indenter", 196 | "once_cell", 197 | "owo-colors", 198 | "tracing-error", 199 | ] 200 | 201 | [[package]] 202 | name = "color-spantrace" 203 | version = "0.2.1" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 206 | dependencies = [ 207 | "once_cell", 208 | "owo-colors", 209 | "tracing-core", 210 | "tracing-error", 211 | ] 212 | 213 | [[package]] 214 | name = "colorchoice" 215 | version = "1.0.2" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 218 | 219 | [[package]] 220 | name = "colored" 221 | version = "2.1.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 224 | dependencies = [ 225 | "lazy_static", 226 | "windows-sys 0.48.0", 227 | ] 228 | 229 | [[package]] 230 | name = "const_format" 231 | version = "0.2.33" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" 234 | dependencies = [ 235 | "const_format_proc_macros", 236 | ] 237 | 238 | [[package]] 239 | name = "const_format_proc_macros" 240 | version = "0.2.33" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" 243 | dependencies = [ 244 | "proc-macro2", 245 | "quote", 246 | "unicode-xid", 247 | ] 248 | 249 | [[package]] 250 | name = "core-foundation-sys" 251 | version = "0.8.7" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 254 | 255 | [[package]] 256 | name = "crossbeam-deque" 257 | version = "0.8.5" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 260 | dependencies = [ 261 | "crossbeam-epoch", 262 | "crossbeam-utils", 263 | ] 264 | 265 | [[package]] 266 | name = "crossbeam-epoch" 267 | version = "0.9.18" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 270 | dependencies = [ 271 | "crossbeam-utils", 272 | ] 273 | 274 | [[package]] 275 | name = "crossbeam-utils" 276 | version = "0.8.20" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 279 | 280 | [[package]] 281 | name = "deranged" 282 | version = "0.3.11" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 285 | dependencies = [ 286 | "powerfmt", 287 | ] 288 | 289 | [[package]] 290 | name = "dirs" 291 | version = "5.0.1" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 294 | dependencies = [ 295 | "dirs-sys", 296 | ] 297 | 298 | [[package]] 299 | name = "dirs-sys" 300 | version = "0.4.1" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 303 | dependencies = [ 304 | "libc", 305 | "option-ext", 306 | "redox_users", 307 | "windows-sys 0.48.0", 308 | ] 309 | 310 | [[package]] 311 | name = "duct" 312 | version = "0.13.7" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" 315 | dependencies = [ 316 | "libc", 317 | "once_cell", 318 | "os_pipe", 319 | "shared_child", 320 | ] 321 | 322 | [[package]] 323 | name = "either" 324 | version = "1.13.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 327 | 328 | [[package]] 329 | name = "env_filter" 330 | version = "0.1.2" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" 333 | dependencies = [ 334 | "log", 335 | "regex", 336 | ] 337 | 338 | [[package]] 339 | name = "env_logger" 340 | version = "0.11.5" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" 343 | dependencies = [ 344 | "anstream", 345 | "anstyle", 346 | "env_filter", 347 | "humantime", 348 | "log", 349 | ] 350 | 351 | [[package]] 352 | name = "equivalent" 353 | version = "1.0.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 356 | 357 | [[package]] 358 | name = "errno" 359 | version = "0.3.9" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 362 | dependencies = [ 363 | "libc", 364 | "windows-sys 0.52.0", 365 | ] 366 | 367 | [[package]] 368 | name = "eyre" 369 | version = "0.6.12" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 372 | dependencies = [ 373 | "indenter", 374 | "once_cell", 375 | ] 376 | 377 | [[package]] 378 | name = "getrandom" 379 | version = "0.2.15" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 382 | dependencies = [ 383 | "cfg-if", 384 | "libc", 385 | "wasi", 386 | ] 387 | 388 | [[package]] 389 | name = "gimli" 390 | version = "0.28.1" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 393 | 394 | [[package]] 395 | name = "hashbrown" 396 | version = "0.14.5" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 399 | 400 | [[package]] 401 | name = "heck" 402 | version = "0.5.0" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 405 | 406 | [[package]] 407 | name = "hex" 408 | version = "0.4.3" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 411 | 412 | [[package]] 413 | name = "humantime" 414 | version = "2.1.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 417 | 418 | [[package]] 419 | name = "indenter" 420 | version = "0.3.3" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 423 | 424 | [[package]] 425 | name = "indexmap" 426 | version = "2.5.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" 429 | dependencies = [ 430 | "equivalent", 431 | "hashbrown", 432 | ] 433 | 434 | [[package]] 435 | name = "is_debug" 436 | version = "1.0.1" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" 439 | 440 | [[package]] 441 | name = "is_terminal_polyfill" 442 | version = "1.70.1" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 445 | 446 | [[package]] 447 | name = "itertools" 448 | version = "0.13.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 451 | dependencies = [ 452 | "either", 453 | ] 454 | 455 | [[package]] 456 | name = "itoa" 457 | version = "1.0.11" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 460 | 461 | [[package]] 462 | name = "lazy_static" 463 | version = "1.5.0" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 466 | 467 | [[package]] 468 | name = "libc" 469 | version = "0.2.158" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 472 | 473 | [[package]] 474 | name = "libredox" 475 | version = "0.1.3" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 478 | dependencies = [ 479 | "bitflags", 480 | "libc", 481 | ] 482 | 483 | [[package]] 484 | name = "linked-hash-map" 485 | version = "0.5.6" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 488 | 489 | [[package]] 490 | name = "linux-raw-sys" 491 | version = "0.4.14" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 494 | 495 | [[package]] 496 | name = "log" 497 | version = "0.4.22" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 500 | 501 | [[package]] 502 | name = "macos-defaults" 503 | version = "0.2.0" 504 | dependencies = [ 505 | "camino", 506 | "clap", 507 | "clap-verbosity-flag", 508 | "clap_complete", 509 | "color-eyre", 510 | "colored", 511 | "dirs", 512 | "duct", 513 | "env_logger", 514 | "hex", 515 | "itertools", 516 | "log", 517 | "plist", 518 | "serde", 519 | "serde_yaml", 520 | "shadow-rs", 521 | "sysinfo", 522 | "testresult", 523 | "thiserror", 524 | "yaml-rust", 525 | "yaml-split", 526 | ] 527 | 528 | [[package]] 529 | name = "memchr" 530 | version = "2.7.4" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 533 | 534 | [[package]] 535 | name = "miniz_oxide" 536 | version = "0.7.4" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 539 | dependencies = [ 540 | "adler", 541 | ] 542 | 543 | [[package]] 544 | name = "ntapi" 545 | version = "0.4.1" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 548 | dependencies = [ 549 | "winapi", 550 | ] 551 | 552 | [[package]] 553 | name = "num-conv" 554 | version = "0.1.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 557 | 558 | [[package]] 559 | name = "num_threads" 560 | version = "0.1.7" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 563 | dependencies = [ 564 | "libc", 565 | ] 566 | 567 | [[package]] 568 | name = "object" 569 | version = "0.32.2" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 572 | dependencies = [ 573 | "memchr", 574 | ] 575 | 576 | [[package]] 577 | name = "once_cell" 578 | version = "1.19.0" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 581 | 582 | [[package]] 583 | name = "option-ext" 584 | version = "0.2.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 587 | 588 | [[package]] 589 | name = "os_pipe" 590 | version = "1.2.1" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" 593 | dependencies = [ 594 | "libc", 595 | "windows-sys 0.59.0", 596 | ] 597 | 598 | [[package]] 599 | name = "owo-colors" 600 | version = "3.5.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 603 | 604 | [[package]] 605 | name = "pin-project-lite" 606 | version = "0.2.14" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 609 | 610 | [[package]] 611 | name = "plist" 612 | version = "1.7.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" 615 | dependencies = [ 616 | "base64", 617 | "indexmap", 618 | "quick-xml", 619 | "serde", 620 | "time", 621 | ] 622 | 623 | [[package]] 624 | name = "powerfmt" 625 | version = "0.2.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 628 | 629 | [[package]] 630 | name = "proc-macro2" 631 | version = "1.0.86" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 634 | dependencies = [ 635 | "unicode-ident", 636 | ] 637 | 638 | [[package]] 639 | name = "quick-xml" 640 | version = "0.32.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" 643 | dependencies = [ 644 | "memchr", 645 | ] 646 | 647 | [[package]] 648 | name = "quote" 649 | version = "1.0.37" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 652 | dependencies = [ 653 | "proc-macro2", 654 | ] 655 | 656 | [[package]] 657 | name = "rayon" 658 | version = "1.10.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 661 | dependencies = [ 662 | "either", 663 | "rayon-core", 664 | ] 665 | 666 | [[package]] 667 | name = "rayon-core" 668 | version = "1.12.1" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 671 | dependencies = [ 672 | "crossbeam-deque", 673 | "crossbeam-utils", 674 | ] 675 | 676 | [[package]] 677 | name = "redox_users" 678 | version = "0.4.6" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 681 | dependencies = [ 682 | "getrandom", 683 | "libredox", 684 | "thiserror", 685 | ] 686 | 687 | [[package]] 688 | name = "regex" 689 | version = "1.10.6" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 692 | dependencies = [ 693 | "aho-corasick", 694 | "memchr", 695 | "regex-automata", 696 | "regex-syntax", 697 | ] 698 | 699 | [[package]] 700 | name = "regex-automata" 701 | version = "0.4.7" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 704 | dependencies = [ 705 | "aho-corasick", 706 | "memchr", 707 | "regex-syntax", 708 | ] 709 | 710 | [[package]] 711 | name = "regex-syntax" 712 | version = "0.8.4" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 715 | 716 | [[package]] 717 | name = "rustc-demangle" 718 | version = "0.1.24" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 721 | 722 | [[package]] 723 | name = "rustix" 724 | version = "0.38.37" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 727 | dependencies = [ 728 | "bitflags", 729 | "errno", 730 | "libc", 731 | "linux-raw-sys", 732 | "windows-sys 0.52.0", 733 | ] 734 | 735 | [[package]] 736 | name = "ryu" 737 | version = "1.0.18" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 740 | 741 | [[package]] 742 | name = "serde" 743 | version = "1.0.210" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 746 | dependencies = [ 747 | "serde_derive", 748 | ] 749 | 750 | [[package]] 751 | name = "serde_derive" 752 | version = "1.0.210" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 755 | dependencies = [ 756 | "proc-macro2", 757 | "quote", 758 | "syn", 759 | ] 760 | 761 | [[package]] 762 | name = "serde_yaml" 763 | version = "0.9.34+deprecated" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 766 | dependencies = [ 767 | "indexmap", 768 | "itoa", 769 | "ryu", 770 | "serde", 771 | "unsafe-libyaml", 772 | ] 773 | 774 | [[package]] 775 | name = "shadow-rs" 776 | version = "0.35.0" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "fca0e9bdc073d7173ba993fb7886477af5df75588b57afcb4b96f21911ab0bfa" 779 | dependencies = [ 780 | "const_format", 781 | "is_debug", 782 | "time", 783 | ] 784 | 785 | [[package]] 786 | name = "sharded-slab" 787 | version = "0.1.7" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 790 | dependencies = [ 791 | "lazy_static", 792 | ] 793 | 794 | [[package]] 795 | name = "shared_child" 796 | version = "1.0.1" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" 799 | dependencies = [ 800 | "libc", 801 | "windows-sys 0.59.0", 802 | ] 803 | 804 | [[package]] 805 | name = "shlex" 806 | version = "1.3.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 809 | 810 | [[package]] 811 | name = "strsim" 812 | version = "0.11.1" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 815 | 816 | [[package]] 817 | name = "syn" 818 | version = "2.0.77" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 821 | dependencies = [ 822 | "proc-macro2", 823 | "quote", 824 | "unicode-ident", 825 | ] 826 | 827 | [[package]] 828 | name = "sysinfo" 829 | version = "0.31.4" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" 832 | dependencies = [ 833 | "core-foundation-sys", 834 | "libc", 835 | "memchr", 836 | "ntapi", 837 | "rayon", 838 | "windows", 839 | ] 840 | 841 | [[package]] 842 | name = "terminal_size" 843 | version = "0.3.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" 846 | dependencies = [ 847 | "rustix", 848 | "windows-sys 0.48.0", 849 | ] 850 | 851 | [[package]] 852 | name = "testresult" 853 | version = "0.4.1" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "614b328ff036a4ef882c61570f72918f7e9c5bee1da33f8e7f91e01daee7e56c" 856 | 857 | [[package]] 858 | name = "thiserror" 859 | version = "1.0.63" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 862 | dependencies = [ 863 | "thiserror-impl", 864 | ] 865 | 866 | [[package]] 867 | name = "thiserror-impl" 868 | version = "1.0.63" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 871 | dependencies = [ 872 | "proc-macro2", 873 | "quote", 874 | "syn", 875 | ] 876 | 877 | [[package]] 878 | name = "thread_local" 879 | version = "1.1.8" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 882 | dependencies = [ 883 | "cfg-if", 884 | "once_cell", 885 | ] 886 | 887 | [[package]] 888 | name = "time" 889 | version = "0.3.36" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 892 | dependencies = [ 893 | "deranged", 894 | "itoa", 895 | "libc", 896 | "num-conv", 897 | "num_threads", 898 | "powerfmt", 899 | "serde", 900 | "time-core", 901 | "time-macros", 902 | ] 903 | 904 | [[package]] 905 | name = "time-core" 906 | version = "0.1.2" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 909 | 910 | [[package]] 911 | name = "time-macros" 912 | version = "0.2.18" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 915 | dependencies = [ 916 | "num-conv", 917 | "time-core", 918 | ] 919 | 920 | [[package]] 921 | name = "tracing" 922 | version = "0.1.40" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 925 | dependencies = [ 926 | "pin-project-lite", 927 | "tracing-core", 928 | ] 929 | 930 | [[package]] 931 | name = "tracing-core" 932 | version = "0.1.32" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 935 | dependencies = [ 936 | "once_cell", 937 | "valuable", 938 | ] 939 | 940 | [[package]] 941 | name = "tracing-error" 942 | version = "0.2.0" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" 945 | dependencies = [ 946 | "tracing", 947 | "tracing-subscriber", 948 | ] 949 | 950 | [[package]] 951 | name = "tracing-subscriber" 952 | version = "0.3.18" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 955 | dependencies = [ 956 | "sharded-slab", 957 | "thread_local", 958 | "tracing-core", 959 | ] 960 | 961 | [[package]] 962 | name = "unicode-ident" 963 | version = "1.0.13" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 966 | 967 | [[package]] 968 | name = "unicode-xid" 969 | version = "0.2.5" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" 972 | 973 | [[package]] 974 | name = "unsafe-libyaml" 975 | version = "0.2.11" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 978 | 979 | [[package]] 980 | name = "utf8parse" 981 | version = "0.2.2" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 984 | 985 | [[package]] 986 | name = "valuable" 987 | version = "0.1.0" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 990 | 991 | [[package]] 992 | name = "wasi" 993 | version = "0.11.0+wasi-snapshot-preview1" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 996 | 997 | [[package]] 998 | name = "winapi" 999 | version = "0.3.9" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1002 | dependencies = [ 1003 | "winapi-i686-pc-windows-gnu", 1004 | "winapi-x86_64-pc-windows-gnu", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "winapi-i686-pc-windows-gnu" 1009 | version = "0.4.0" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1012 | 1013 | [[package]] 1014 | name = "winapi-x86_64-pc-windows-gnu" 1015 | version = "0.4.0" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1018 | 1019 | [[package]] 1020 | name = "windows" 1021 | version = "0.57.0" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" 1024 | dependencies = [ 1025 | "windows-core", 1026 | "windows-targets 0.52.6", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "windows-core" 1031 | version = "0.57.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" 1034 | dependencies = [ 1035 | "windows-implement", 1036 | "windows-interface", 1037 | "windows-result", 1038 | "windows-targets 0.52.6", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "windows-implement" 1043 | version = "0.57.0" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" 1046 | dependencies = [ 1047 | "proc-macro2", 1048 | "quote", 1049 | "syn", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "windows-interface" 1054 | version = "0.57.0" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" 1057 | dependencies = [ 1058 | "proc-macro2", 1059 | "quote", 1060 | "syn", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "windows-result" 1065 | version = "0.1.2" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" 1068 | dependencies = [ 1069 | "windows-targets 0.52.6", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "windows-sys" 1074 | version = "0.48.0" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1077 | dependencies = [ 1078 | "windows-targets 0.48.5", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "windows-sys" 1083 | version = "0.52.0" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1086 | dependencies = [ 1087 | "windows-targets 0.52.6", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "windows-sys" 1092 | version = "0.59.0" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1095 | dependencies = [ 1096 | "windows-targets 0.52.6", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "windows-targets" 1101 | version = "0.48.5" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1104 | dependencies = [ 1105 | "windows_aarch64_gnullvm 0.48.5", 1106 | "windows_aarch64_msvc 0.48.5", 1107 | "windows_i686_gnu 0.48.5", 1108 | "windows_i686_msvc 0.48.5", 1109 | "windows_x86_64_gnu 0.48.5", 1110 | "windows_x86_64_gnullvm 0.48.5", 1111 | "windows_x86_64_msvc 0.48.5", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "windows-targets" 1116 | version = "0.52.6" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1119 | dependencies = [ 1120 | "windows_aarch64_gnullvm 0.52.6", 1121 | "windows_aarch64_msvc 0.52.6", 1122 | "windows_i686_gnu 0.52.6", 1123 | "windows_i686_gnullvm", 1124 | "windows_i686_msvc 0.52.6", 1125 | "windows_x86_64_gnu 0.52.6", 1126 | "windows_x86_64_gnullvm 0.52.6", 1127 | "windows_x86_64_msvc 0.52.6", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "windows_aarch64_gnullvm" 1132 | version = "0.48.5" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1135 | 1136 | [[package]] 1137 | name = "windows_aarch64_gnullvm" 1138 | version = "0.52.6" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1141 | 1142 | [[package]] 1143 | name = "windows_aarch64_msvc" 1144 | version = "0.48.5" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1147 | 1148 | [[package]] 1149 | name = "windows_aarch64_msvc" 1150 | version = "0.52.6" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1153 | 1154 | [[package]] 1155 | name = "windows_i686_gnu" 1156 | version = "0.48.5" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1159 | 1160 | [[package]] 1161 | name = "windows_i686_gnu" 1162 | version = "0.52.6" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1165 | 1166 | [[package]] 1167 | name = "windows_i686_gnullvm" 1168 | version = "0.52.6" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1171 | 1172 | [[package]] 1173 | name = "windows_i686_msvc" 1174 | version = "0.48.5" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1177 | 1178 | [[package]] 1179 | name = "windows_i686_msvc" 1180 | version = "0.52.6" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1183 | 1184 | [[package]] 1185 | name = "windows_x86_64_gnu" 1186 | version = "0.48.5" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1189 | 1190 | [[package]] 1191 | name = "windows_x86_64_gnu" 1192 | version = "0.52.6" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1195 | 1196 | [[package]] 1197 | name = "windows_x86_64_gnullvm" 1198 | version = "0.48.5" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1201 | 1202 | [[package]] 1203 | name = "windows_x86_64_gnullvm" 1204 | version = "0.52.6" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1207 | 1208 | [[package]] 1209 | name = "windows_x86_64_msvc" 1210 | version = "0.48.5" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1213 | 1214 | [[package]] 1215 | name = "windows_x86_64_msvc" 1216 | version = "0.52.6" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1219 | 1220 | [[package]] 1221 | name = "yaml-rust" 1222 | version = "0.4.5" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 1225 | dependencies = [ 1226 | "linked-hash-map", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "yaml-split" 1231 | version = "0.4.0" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "9dab2bfe3b9aa09e8424e0e5139526c6a3857c4bd334d66b0453a357dd80fc58" 1234 | dependencies = [ 1235 | "thiserror", 1236 | ] 1237 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = [ "Dan Sully " ] 3 | build = "build.rs" 4 | categories = [ 5 | "command-line-utilities", 6 | "config", 7 | "os::macos-apis", 8 | ] 9 | description = "Defaults setting for macOS" 10 | edition = "2021" 11 | homepage = "https://github.com/dsully/macos-defaults" 12 | keywords = [ "macos", "user", "defaults", "declarative", "yaml" ] 13 | license = "MIT" 14 | name = "macos-defaults" 15 | readme = "README.md" 16 | repository = "https://github.com/dsully/macos-defaults" 17 | version = "0.2.0" 18 | 19 | [dependencies] 20 | camino = "1.1.9" 21 | clap = { version = "~4.5.17", features = [ 22 | "cargo", 23 | "color", 24 | "derive", 25 | "suggestions", 26 | "wrap_help", 27 | ] } 28 | clap-verbosity-flag = "2.2.1" 29 | clap_complete = "4.5.28" 30 | color-eyre = "0.6.3" 31 | colored = "2.1.0" 32 | dirs = "5.0.1" 33 | duct = "0.13.7" 34 | env_logger = "0.11.5" 35 | hex = "0.4.3" 36 | itertools = "0.13.0" 37 | log = "0.4.22" 38 | plist = "1.7.0" 39 | serde = { version = "1.0.210", features = [ "derive" ] } 40 | serde_yaml = "0.9.34" 41 | shadow-rs = { version = "0.35.0", default-features = false } 42 | sysinfo = "0.31.4" 43 | thiserror = "1.0.63" 44 | yaml-rust = "0.4.5" 45 | yaml-split = "0.4.0" 46 | 47 | [dev-dependencies] 48 | testresult = "0.4.1" 49 | 50 | [build-dependencies] 51 | shadow-rs = { version = "0.35.0", default-features = false } 52 | 53 | [lints.clippy] 54 | pedantic = "deny" 55 | 56 | # Config for 'cargo dist' 57 | [workspace.metadata.dist] 58 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 59 | cargo-dist-version = "0.22.1" 60 | # CI backends to support 61 | ci = "github" 62 | # The installers to generate for each app 63 | installers = [ "homebrew" ] 64 | # A GitHub repo to push Homebrew formulas to 65 | tap = "dsully/homebrew-tap" 66 | # Target platforms to build apps for (Rust target-triple syntax) 67 | targets = [ "aarch64-apple-darwin" ] 68 | # Publish jobs to run in CI 69 | publish-jobs = [ "homebrew" ] 70 | # Which actions to run on pull requests 71 | pr-run-mode = "plan" 72 | 73 | # Use Apple Silicon runners. 74 | [workspace.metadata.dist.github-custom-runners] 75 | aarch64-apple-darwin = "macos-14" 76 | 77 | # The profile that 'cargo dist' will build with 78 | [profile.dist] 79 | codegen-units = 1 80 | debug = false 81 | incremental = false 82 | inherits = "release" 83 | lto = true 84 | opt-level = 3 85 | panic = "abort" 86 | strip = "none" 87 | 88 | [profile.dev] 89 | debug = 0 90 | 91 | [profile.release] 92 | codegen-units = 1 93 | lto = true 94 | panic = "abort" 95 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | default: build test 2 | 3 | build: 4 | @cargo build --all 5 | 6 | check: 7 | @cargo check --all 8 | 9 | format: 10 | @cargo fmt --all 11 | 12 | format-check: 13 | @cargo fmt --all -- --check 14 | 15 | lint: 16 | @cargo clippy --all -- -D clippy::dbg-macro -D warnings 17 | 18 | test: 19 | @cargo test --all 20 | 21 | patch: 22 | @cargo release version patch --execute 23 | 24 | minor: 25 | @cargo release version minor --execute 26 | 27 | major: 28 | @cargo release version major --execute 29 | 30 | udeps: 31 | RUSTC_BOOTSTRAP=1 cargo +nightly udeps --all-targets --backend depinfo 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dan Sully 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macos-defaults 2 | 3 | A tool for managing macOS defaults declaratively via YAML files. 4 | 5 | ## Install 6 | 7 | ### Homebrew 8 | 9 | ```shell 10 | brew install dsully/tap/macos-defaults 11 | ``` 12 | 13 | ### Source 14 | 15 | ```shell 16 | cargo install --git https://github.com/dsully/macos-defaults 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Dump a defaults domain to YAML 22 | 23 | ```shell 24 | # To stdout: 25 | macos-defaults dump -d com.apple.Dock 26 | 27 | # To a file: 28 | macos-defaults dump -d com.apple.Dock dock.yaml 29 | 30 | # Global domain 31 | macos-defaults dump -g 32 | ``` 33 | 34 | ### Apply defaults from a YAML file 35 | 36 | ```shell 37 | # From a single YAML file: 38 | macos-defaults apply dock.yaml 39 | 40 | # From a directory with YAML files & debug logging: 41 | macos-defaults apply -vvv ~/.config/macos-defaults/ 42 | ``` 43 | 44 | ### Generate shell completions 45 | 46 | ```shell 47 | macos-defaults completions [bash|fish|zsh] > ~/.config/fish/completions/macos-defaults.fish 48 | ``` 49 | 50 | See `macos-defaults --help` for more details. 51 | 52 | ## YAML Format 53 | 54 | ```yaml 55 | --- 56 | # This will be printed to stdout. 57 | description: Contacts 58 | 59 | # Use the currentHost hardware UUID to find the correct plist file. 60 | # https://apple.stackexchange.com/questions/353528/what-is-currenthost-for-in-defaults 61 | current_host: false 62 | 63 | # Send a SIGTERM to one or more processes if any defaults were changed. 64 | kill: ["Contacts", "cfprefsd"] 65 | 66 | # A nested map of plist domains to key/value pairs to set. 67 | data: 68 | # Show first name 69 | # 1 = before last name 70 | # 2 = after last name 71 | NSGlobalDomain: 72 | NSPersonNameDefaultDisplayNameOrder: 1 73 | 74 | # Sort by 75 | com.apple.AddressBook: 76 | ABNameSortingFormat: "sortingFirstName sortingLastName" 77 | 78 | # vCard format 79 | # false = 3.0 80 | # true = 2.1 81 | ABUse21vCardFormat: false 82 | 83 | # Enable private me vCard 84 | ABPrivateVCardFieldsEnabled: false 85 | 86 | # Export notes in vCards 87 | ABIncludeNotesInVCard: true 88 | 89 | # Export photos in vCards 90 | ABIncludePhotosInVCard: true 91 | --- 92 | # Multiple yaml docs in single file. 93 | description: Dock 94 | 95 | kill: ["Dock"] 96 | 97 | data: 98 | # Automatically hide and show the Dock 99 | com.apple.dock: 100 | autohide: true 101 | ``` 102 | 103 | You may also use full paths to `.plist` files instead of domain names. This is the only way to set values in /Library/Preferences/. 104 | 105 | ### Overwrite syntax 106 | 107 | By default, the YAML will be merged against existing domains. 108 | 109 | For example, the following config will leave any other keys on `DesktopViewSettings:IconViewSettings` untouched: 110 | ```yaml 111 | data: 112 | com.apple.finder: 113 | DesktopViewSettings: 114 | IconViewSettings: 115 | labelOnBottom: false # item info on right 116 | iconSize: 80.0 117 | ``` 118 | 119 | This can be overridden by adding the key `"!"` to a dict, which will delete any keys which are not specified. For example, the following config will delete all properties on the com.apple.finder domain except for DesktopViewSettings, and likewise, all properties on `IconViewSettings` except those specified. 120 | 121 | ```yaml 122 | data: 123 | com.apple.finder: 124 | "!": {} # overwrite! 125 | DesktopViewSettings: 126 | IconViewSettings: 127 | "!": {} # overwrite! 128 | labelOnBottom: false # item info on right 129 | iconSize: 80.0 130 | ``` 131 | 132 | This feature has the potential to erase important settings, so exercise caution. Running `macos-defaults apply` creates a backup of each modified plist at, for example, `~/Library/Preferences/com.apple.finder.plist.prev`. 133 | 134 | ### Array merge syntax 135 | 136 | If an array contains the element `"..."`, it will be replaced by the contents of the existing array. Arrays are treated like sets, so elements which already exist will not be added. 137 | 138 | For example, the following config: 139 | 140 | ```yaml 141 | data: 142 | org.my.test: 143 | aDict: 144 | }; 145 | anArray: ["foo", "...", "bar"] 146 | ``` 147 | 148 | * Prepend `"foo"` to `aDict:anArray`, if it doesn't already contain `"foo"`. 149 | * Append `"bar"` to `aDict:anArray`, if it doesn't already contain `"bar"`. 150 | 151 | ## Examples 152 | 153 | See my [dotfiles](https://github.com/dsully/dotfiles/tree/main/.data/macos-defaults) repository. 154 | 155 | ## On YAML 156 | 157 | ![Yelling At My Laptop](docs/YAML.jpg?raw=true) 158 | 159 | [YAML](https://yaml.org) is not a format I prefer, but out of common formats it unfortunately had the most properties I wanted. 160 | 161 | * [JSON](https://en.wikipedia.org/wiki/JSON) doesn't have comments and is overly verbose (JSONC/JSON5 is not common) 162 | 163 | * [XML](https://en.wikipedia.org/wiki/XML): No. 164 | 165 | * [INI](https://en.wikipedia.org/wiki/INI_file) is too limited. 166 | 167 | * [TOML](https://toml.io/en/) is overly verbose and is surprisingly not that easy to work with in Rust. Deeply nested maps are poorly handled. 168 | 169 | * [KDL](https://kdl.dev) is nice, but document oriented & needs struct annotations. Derive is implemented in the 3rd party [Knuffle](https://docs.rs/knuffel/latest/knuffel/) crate. 170 | 171 | * [RON](https://github.com/ron-rs/ron) is Rust specific, so editor support isn't there. 172 | 173 | * [KCL](https://kcl-lang.io), [CUE](https://cuelang.org), [HCL](https://github.com/hashicorp/hcl), too high level & not appropriate for the task. 174 | 175 | So YAML it is. 176 | 177 | ## Inspiration 178 | 179 | This tool was heavily inspired by and uses code from [up-rs](https://github.com/gibfahn/up-rs) 180 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // https://crates.io/crates/shadow-rs 2 | fn main() -> shadow_rs::SdResult<()> { 3 | shadow_rs::new() 4 | } 5 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.70.0" 2 | -------------------------------------------------------------------------------- /docs/YAML.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsully/macos-defaults/eb9738c068618f36d051052036a73193d8249971/docs/YAML.jpg -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Unstable 2 | # blank_lines_lower_bound = 1 3 | # imports_granularity = "Module" 4 | 5 | edition = "2021" 6 | max_width = 160 7 | use_field_init_shorthand = true 8 | -------------------------------------------------------------------------------- /src/cmd/apply.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::{BufReader, BufRead}; 3 | use std::ffi::OsStr; 4 | use std::fs::File; 5 | use std::os::unix::ffi::OsStrExt; 6 | 7 | use camino::Utf8PathBuf; 8 | use color_eyre::eyre::{eyre, Result, WrapErr}; 9 | use colored::Colorize; 10 | use log::{debug, error, trace}; 11 | use serde::{Deserialize, Serialize}; 12 | use sysinfo::{Signal, System}; 13 | use yaml_split::DocumentIterator; 14 | 15 | use crate::defaults::{write_defaults_values, MacOSDefaults}; 16 | use crate::errors::DefaultsError as E; 17 | 18 | /* 19 | // NB: Some of this code originated from: https://github.com/gibfahn/up-rs, MIT & Apache 2.0 licensed. 20 | 21 | Update macOS defaults. 22 | 23 | Make it easy for users to provide a list of defaults to update, and run all 24 | the updates at once. Also takes care of restarting any tools to pick up the 25 | config, or notifying the user if they need to log out or reboot. 26 | 27 | Note that manually editing .plist files on macOS (rather than using e.g. the `defaults` binary) 28 | may cause changes not to be picked up until `cfprefsd` is restarted 29 | ([more information](https://eclecticlight.co/2017/07/06/sticky-preferences-why-trashing-or-editing-them-may-not-change-anything/)). 30 | 31 | Work around this by adding `kill: ["cfprefsd"]` to the YAML file. 32 | 33 | ## Specifying preference domains 34 | 35 | For normal preference domains, you can directly specify the domain as a key, so to set `defaults read NSGlobalDomain com.apple.swipescrolldirection` you would use: 36 | 37 | ```yaml 38 | kill: ["cfprefsd"] 39 | data: 40 | NSGlobalDomain: 41 | com.apple.swipescrolldirection: false 42 | ``` 43 | 44 | You can also use a full path to a plist file (the `.plist` file extension is optional, as with the `defaults` command). 45 | 46 | ## Current Host modifications 47 | 48 | To modify defaults for the current host, you will need to add a `current_host: true` key/value pair: 49 | 50 | e.g. to set the preference returned by `defaults -currentHost read -globalDomain com.apple.mouse.tapBehavior` you would have: 51 | 52 | ```yaml 53 | kill: ["cfprefsd"] 54 | current_host: true 55 | data: 56 | NSGlobalDomain: 57 | # Enable Tap to Click for the current user. 58 | com.apple.mouse.tapBehavior: 1 59 | ``` 60 | 61 | ## Root-owned Defaults 62 | 63 | To write to files owned by root, set the `sudo: true` environment variable, and use the full path to the preferences file. 64 | 65 | ```yaml 66 | kill: cfprefsd 67 | sudo: true 68 | data: 69 | # System Preferences -> Users & Groups -> Login Options -> Show Input menu in login window 70 | /Library/Preferences/com.apple.loginwindow: 71 | showInputMenu: true 72 | 73 | # System Preferences -> Software Update -> Automatically keep my mac up to date 74 | /Library/Preferences/com.apple.SoftwareUpdate: 75 | AutomaticDownload: true 76 | ``` 77 | 78 | */ 79 | 80 | // Dummy struct before YAML deserialization attempt. 81 | #[derive(Debug, Default, Serialize, Deserialize)] 82 | struct DefaultsConfig(HashMap>); 83 | 84 | pub fn apply_defaults(path: &Utf8PathBuf) -> Result { 85 | // 86 | let file = File::open(path).map_err(|e| E::FileRead { 87 | path: path.to_owned(), 88 | source: e, 89 | })?; 90 | 91 | let reader = BufReader::new(file); 92 | 93 | trace!("Processing YAML documents from file: {}", path); 94 | 95 | let mut any_changed = false; 96 | 97 | for doc in DocumentIterator::new(reader) { 98 | let doc = doc.map_err(|e| E::YamlSplitError { 99 | path: path.to_owned(), 100 | source: e, 101 | })?; 102 | any_changed |= process_yaml_document(doc.as_bytes(), path)?; 103 | } 104 | 105 | Ok(any_changed) 106 | } 107 | 108 | fn process_yaml_document(doc: impl BufRead, path: &Utf8PathBuf) -> Result { 109 | let config: MacOSDefaults = serde_yaml::from_reader(doc).map_err(|e| E::InvalidYaml { 110 | path: path.to_owned(), 111 | source: e, 112 | })?; 113 | 114 | let maybe_data = config.data.ok_or_else(|| eyre!("Couldn't parse YAML data key in: {path}"))?; 115 | 116 | let defaults: DefaultsConfig = serde_yaml::from_value(maybe_data).map_err(|e| E::DeserializationFailed { source: e })?; 117 | 118 | debug!("Setting defaults"); 119 | 120 | // TODO: Get global CLI verbosity values. 121 | if let Some(description) = config.description { 122 | println!(" {} {}", "▶".green(), description.bold().white()); 123 | } 124 | 125 | let results: Vec<_> = defaults.0 126 | .into_iter() 127 | .map(|(domain, prefs)| write_defaults_values(&domain, prefs, config.current_host)) 128 | .collect(); 129 | 130 | let (passed, errors): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok); 131 | 132 | let changed = passed.iter().any(|r| *r.as_ref().unwrap()); 133 | 134 | if changed { 135 | if let Some(kill) = config.kill { 136 | for process in kill { 137 | println!(" {} Restarting: {}", "✖".blue(), process.white()); 138 | kill_process_by_name(&process); 139 | } 140 | } 141 | } 142 | 143 | if errors.is_empty() { 144 | return Ok(changed); 145 | } 146 | 147 | for error in &errors { 148 | error!("{error:?}"); 149 | } 150 | 151 | let mut errors_iter = errors.into_iter(); 152 | 153 | let first_error = errors_iter.next().ok_or(E::UnexpectedNone)??; 154 | 155 | Err(eyre!("{:?}", errors_iter.collect::>())).wrap_err(first_error) 156 | } 157 | 158 | fn kill_process_by_name(name: &str) { 159 | let mut sys = System::new(); 160 | sys.refresh_processes(sysinfo::ProcessesToUpdate::All); 161 | 162 | for process in sys.processes_by_exact_name(OsStr::from_bytes(name.as_bytes())) { 163 | debug!("Process running: {} {}", process.pid(), process.name().to_string_lossy()); 164 | 165 | process.kill_with(Signal::Term); 166 | } 167 | } 168 | 169 | fn is_yaml(path: &Utf8PathBuf) -> bool { 170 | path.extension().map(str::to_ascii_lowercase).is_some_and(|ext| ext == "yml" || ext == "yaml") 171 | } 172 | 173 | pub fn process_path(path: Utf8PathBuf) -> Result> { 174 | match path { 175 | path if path.is_file() => Ok(vec![path]), 176 | path if path.is_dir() => { 177 | let mut files = path 178 | .read_dir_utf8()? 179 | .filter_map(Result::ok) 180 | .map(camino::Utf8DirEntry::into_path) 181 | .filter(is_yaml) 182 | .collect::>(); 183 | 184 | files.sort(); 185 | 186 | if files.is_empty() { 187 | Err(eyre!("No YAML files were found in path {path}.")) 188 | } else { 189 | Ok(files) 190 | } 191 | } 192 | _ => Err(eyre!("Couldn't read YAML from: {path}.")), 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/cmd/dump.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | 4 | use camino::Utf8PathBuf; 5 | use color_eyre::eyre::Result; 6 | use log::{debug, trace, warn}; 7 | use plist::{Dictionary, Value}; 8 | use yaml_rust::{YamlEmitter, YamlLoader}; 9 | 10 | use crate::defaults::{get_plist_value_type, plist_path, replace_data_in_plist, MacOSDefaults, NS_GLOBAL_DOMAIN}; 11 | use crate::errors::DefaultsError as E; 12 | 13 | /// `dump` command. 14 | pub fn dump(current_host: bool, output: Option, global_domain: bool, domain: Option) -> Result<()> { 15 | // 16 | let domain = if global_domain { 17 | NS_GLOBAL_DOMAIN.to_owned() 18 | } else { 19 | domain.ok_or(E::MissingDomain {})? 20 | }; 21 | 22 | debug!("Domain: {domain:?}"); 23 | let plist_path = plist_path(&domain, current_host)?; 24 | debug!("Plist path: {plist_path}"); 25 | 26 | // TODO: Nicer error. 27 | let plist: Value = plist::from_file(&plist_path).map_err(|e| E::PlistRead { path: plist_path, source: e })?; 28 | 29 | trace!("Plist: {plist:?}"); 30 | 31 | // First pass. 32 | let plist = if serde_yaml::to_string(&plist).is_ok() { 33 | plist 34 | } else { 35 | warn!( 36 | "Serializing plist value to YAML failed, assuming this is because it contained binary \ 37 | data and replacing that with hex-encoded binary data. This is incorrect, but allows \ 38 | the output to be printed." 39 | ); 40 | let mut value = plist.clone(); 41 | 42 | replace_data_in_plist(&mut value).map_err(|e| E::EyreError { source: e })?; 43 | 44 | serde_yaml::to_string(&value).map_err(|e| E::SerializationFailed { 45 | domain: domain.clone(), 46 | source: e, 47 | })?; 48 | value 49 | }; 50 | 51 | // Sort the top level keys. 52 | let mut value = plist 53 | .as_dictionary() 54 | .ok_or_else(|| E::NotADictionary { 55 | domain: domain.clone(), 56 | key: "Unknown".to_owned(), 57 | plist_type: get_plist_value_type(&plist), 58 | })? 59 | .clone(); 60 | 61 | value.sort_keys(); 62 | 63 | let data = serde_yaml::to_value(Dictionary::from_iter(vec![(domain.clone(), Value::Dictionary(value))]))?; 64 | 65 | // Wrap in the container struct. 66 | let defaults = MacOSDefaults { 67 | description: Some(domain), 68 | current_host, 69 | kill: None, 70 | sudo: false, 71 | data: Some(data), 72 | }; 73 | 74 | // Round-trip for yamllint valid YAML. 75 | let yaml = round_trip_yaml(&defaults)?; 76 | 77 | match output { 78 | Some(path) => File::create(path)?.write(&yaml), 79 | None => std::io::stdout().write(&yaml), 80 | }?; 81 | 82 | Ok(()) 83 | } 84 | 85 | fn round_trip_yaml(defaults: &MacOSDefaults) -> Result> { 86 | // 87 | let mut buffer = Vec::new(); 88 | 89 | for doc in YamlLoader::load_from_str(&serde_yaml::to_string(&defaults)?)? { 90 | let mut content = String::new(); 91 | 92 | let mut emitter = YamlEmitter::new(&mut content); 93 | emitter.compact(false); 94 | emitter.dump(&doc).ok(); 95 | 96 | buffer.write_all(content.as_ref())?; 97 | } 98 | 99 | Ok(buffer) 100 | } 101 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apply; 2 | pub mod dump; 3 | 4 | pub use apply::{apply_defaults, process_path}; 5 | pub use dump::dump; 6 | -------------------------------------------------------------------------------- /src/defaults.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for updating plist files. 2 | // 3 | // NB: Most of this code originated from: https://github.com/gibfahn/up-rs, MIT & Apache 2.0 licensed. 4 | 5 | use std::collections::HashMap; 6 | use std::fs::{self, File}; 7 | use std::io::Read; 8 | use std::mem; 9 | 10 | use camino::{Utf8Path, Utf8PathBuf}; 11 | use color_eyre::eyre::{eyre, Result}; 12 | use duct::cmd; 13 | use log::{debug, info, trace, warn}; 14 | use plist::{Dictionary, Value}; 15 | use serde::{Deserialize, Serialize}; 16 | 17 | use super::errors::DefaultsError as E; 18 | 19 | /// A value in an array that means "insert existing values here" 20 | const ELLIPSIS: &str = "..."; 21 | /// A value in a dictionary or domain that means "delete any keys not specified here". 22 | const BANG: &str = "!"; 23 | 24 | pub const NS_GLOBAL_DOMAIN: &str = "NSGlobalDomain"; 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | #[serde(deny_unknown_fields)] 28 | pub(super) struct MacOSDefaults { 29 | /// Description of the task. 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub description: Option, 32 | 33 | /// List of processes to kill if updates were needed. 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub kill: Option>, 36 | 37 | /// Set to true to prompt for superuser privileges before running. 38 | /// This will allow all subtasks that up executes in this iteration. 39 | #[serde(default = "default_false")] 40 | pub sudo: bool, 41 | 42 | /// Set to true to use the current host / hardware UUID for defaults. 43 | #[serde(default = "default_false")] 44 | pub current_host: bool, 45 | 46 | // This field must be the last one in order for the yaml serializer in the generate functions 47 | // to be able to serialise it properly. 48 | /// Set of data provided to the Run library. 49 | #[serde(skip_serializing_if = "Option::is_none")] 50 | pub data: Option, 51 | } 52 | 53 | /// Used for serde defaults above. 54 | const fn default_false() -> bool { 55 | false 56 | } 57 | 58 | /** 59 | Get the path to the plist file given a domain. 60 | 61 | This function does not handle root-owned preferences, e.g. those at `/Library/Preferences/`. 62 | 63 | ## Preferences Locations 64 | 65 | Working out the rules for preferences was fairly complex, but if you run `defaults domains` then 66 | you can work out which plist files are actually being read on the machine. 67 | 68 | As far as I can tell, the rules are: 69 | 70 | - `NSGlobalDomain` -> `~/Library/Preferences/.GlobalPreferences.plist` 71 | - `~/Library/Containers/{domain}/Data/Library/Preferences/{domain}.plist` if it exists. 72 | - `~/Library/Preferences/{domain}.plist` 73 | 74 | If none of these exist then create `~/Library/Preferences/{domain}.plist`. 75 | 76 | Note that `defaults domains` actually prints out 77 | `~/Library/Containers/{*}/Data/Library/Preferences/{*}.plist` (i.e. any plist file name inside 78 | a container folder), but `defaults read` only actually checks 79 | `~/Library/Containers/{domain}/Data/Library/Preferences/{domain}.plist` (a plist file whose name 80 | matches the container folder. 81 | 82 | ### Useful Resources 83 | 84 | - [macOS Containers and defaults](https://lapcatsoftware.com/articles/containers.html) 85 | - [Preference settings: where to find them in Mojave](https://eclecticlight.co/2019/08/28/preference-settings-where-to-find-them-in-mojave/) 86 | */ 87 | pub(super) fn plist_path(domain: &str, current_host: bool) -> Result { 88 | // User passed an absolute path -> use it directly. 89 | if domain.starts_with('/') { 90 | return Ok(Utf8PathBuf::from(domain)); 91 | } 92 | 93 | let home_dir = dirs::home_dir().ok_or_else(|| eyre!("Expected to be able to calculate the user's home directory."))?; 94 | let home_dir = Utf8PathBuf::try_from(home_dir)?; 95 | 96 | // Global Domain -> hard coded value. 97 | if domain == NS_GLOBAL_DOMAIN { 98 | let mut plist_path = home_dir; 99 | let filename = plist_filename(".GlobalPreferences", current_host)?; 100 | extend_with_prefs_folders(current_host, &mut plist_path, &filename); 101 | return Ok(plist_path); 102 | } 103 | 104 | // If passed com.foo.bar.plist, trim it to com.foo.bar 105 | let domain = domain.trim_end_matches(".plist"); 106 | let filename = plist_filename(domain, current_host)?; 107 | 108 | let mut sandboxed_plist_path = home_dir.clone(); 109 | sandboxed_plist_path.extend(&["Library", "Containers", domain, "Data"]); 110 | extend_with_prefs_folders(current_host, &mut sandboxed_plist_path, &filename); 111 | 112 | if sandboxed_plist_path.exists() { 113 | trace!("Sandboxed plist path exists."); 114 | return Ok(sandboxed_plist_path); 115 | } 116 | 117 | // let library_plist_path = Utf8PathBuf::from(format!("/Library/Preferences/{filename}")); 118 | // 119 | // if library_plist_path.exists() { 120 | // trace!("/Library plist path exists."); 121 | // return Ok(library_plist_path); 122 | // } 123 | 124 | trace!("Sandboxed plist path does not exist."); 125 | let mut plist_path = home_dir; 126 | extend_with_prefs_folders(current_host, &mut plist_path, &filename); 127 | 128 | // We return this even if it doesn't yet exist. 129 | Ok(plist_path) 130 | } 131 | 132 | /// Take a directory path, and add on the directories and files containing the application's 133 | /// preferences. Normally this is `./Library/Preferences/{domain}.plist`, but if `current_host` is 134 | /// `true`, then we need to look in the `ByHost` subfolder. 135 | fn extend_with_prefs_folders(current_host: bool, plist_path: &mut Utf8PathBuf, filename: &str) { 136 | if current_host { 137 | plist_path.extend(&["Library", "Preferences", "ByHost", filename]); 138 | } else { 139 | plist_path.extend(&["Library", "Preferences", filename]); 140 | } 141 | } 142 | 143 | /// Get the expected filename for a plist file. Normally it's just the preference name + `.plist`, 144 | /// but if it's a currentHost setup, then we need to include the current host UUID as well. 145 | fn plist_filename(domain: &str, current_host: bool) -> Result { 146 | if current_host { 147 | return Ok(format!( 148 | "{domain}.{hardware_uuid}.plist", 149 | hardware_uuid = get_hardware_uuid().map_err(|e| E::EyreError { source: e })? 150 | )); 151 | } 152 | 153 | Ok(format!("{domain}.plist")) 154 | } 155 | 156 | /// String representation of a plist Value's type. 157 | pub(super) fn get_plist_value_type(plist: &plist::Value) -> &'static str { 158 | match plist { 159 | p if p.as_array().is_some() => "array", 160 | p if p.as_boolean().is_some() => "boolean", 161 | p if p.as_date().is_some() => "date", 162 | p if p.as_real().is_some() => "real", 163 | p if p.as_signed_integer().is_some() => "signed_integer", 164 | p if p.as_unsigned_integer().is_some() => "unsigned_integer", 165 | p if p.as_string().is_some() => "string", 166 | p if p.as_dictionary().is_some() => "dictionary", 167 | p if p.as_data().is_some() => "data", 168 | _ => "unknown", 169 | } 170 | } 171 | 172 | /// Check whether a plist file is in the binary plist format or the XML plist format. 173 | fn is_binary(file: &Utf8Path) -> Result { 174 | let mut f = File::open(file).map_err(|e| E::FileRead { 175 | path: file.to_path_buf(), 176 | source: e, 177 | })?; 178 | let mut magic = [0; 8]; 179 | 180 | // read exactly 8 bytes 181 | f.read_exact(&mut magic).map_err(|e| E::FileRead { 182 | path: file.to_path_buf(), 183 | source: e, 184 | })?; 185 | 186 | Ok(&magic == b"bplist00") 187 | } 188 | 189 | /// Write a `HashMap` of key-value pairs to a plist file. 190 | pub(super) fn write_defaults_values(domain: &str, mut prefs: HashMap, current_host: bool) -> Result { 191 | let plist_path = plist_path(domain, current_host)?; 192 | 193 | debug!("Plist path: {plist_path}"); 194 | 195 | let plist_path_exists = plist_path.exists(); 196 | 197 | let mut plist_value: plist::Value = if plist_path_exists { 198 | plist::from_file(&plist_path).map_err(|e| E::PlistRead { 199 | path: plist_path.clone(), 200 | source: e, 201 | })? 202 | } else { 203 | plist::Value::Dictionary(Dictionary::new()) 204 | }; 205 | 206 | trace!("Plist: {plist_value:?}"); 207 | 208 | // Whether we changed anything. 209 | let mut values_changed = false; 210 | 211 | // If we have a key "!", wipe out the existing array. 212 | if prefs.contains_key(BANG) { 213 | plist_value = Value::from(Dictionary::new()); 214 | prefs.remove(BANG); 215 | } 216 | 217 | for (key, mut new_value) in prefs { 218 | let old_value = plist_value 219 | .as_dictionary() 220 | .ok_or_else(|| E::NotADictionary { 221 | domain: domain.to_owned(), 222 | key: key.clone(), 223 | plist_type: get_plist_value_type(&plist_value), 224 | })? 225 | .get(&key); 226 | 227 | debug!( 228 | "Working out whether we need to change the default {domain} {key}: {old_value:?} -> \ 229 | {new_value:?}" 230 | ); 231 | 232 | // Performs merge operations 233 | merge_value(&mut new_value, old_value); 234 | 235 | if let Some(old_value) = old_value { 236 | if old_value == &new_value { 237 | trace!("Nothing to do, values already match: {key:?} = {new_value:?}"); 238 | continue; 239 | } 240 | } 241 | 242 | values_changed = true; 243 | 244 | info!("Changing default {domain} {key}: {old_value:?} -> {new_value:?}",); 245 | 246 | let plist_type = get_plist_value_type(&plist_value); 247 | 248 | trace!("Plist type: {plist_type:?}"); 249 | 250 | plist_value 251 | .as_dictionary_mut() 252 | .ok_or_else(|| E::NotADictionary { 253 | domain: domain.to_owned(), 254 | key: key.clone(), 255 | plist_type, 256 | })? 257 | .insert(key, new_value); 258 | } 259 | 260 | if !values_changed { 261 | return Ok(values_changed); 262 | } 263 | 264 | if plist_path_exists { 265 | let backup_path = Utf8PathBuf::from(format!("{plist_path}.prev")); 266 | 267 | trace!("Backing up plist file {plist_path} -> {backup_path}",); 268 | 269 | // TODO: Handle sudo case and not being able to backup. 270 | fs::copy(&plist_path, &backup_path).map_err(|e| E::FileCopy { 271 | from_path: plist_path.clone(), 272 | to_path: backup_path.clone(), 273 | source: e, 274 | })?; 275 | } else { 276 | warn!("Defaults plist doesn't exist, creating it: {plist_path}"); 277 | 278 | let plist_dirpath = plist_path.parent().ok_or(E::UnexpectedNone)?; 279 | 280 | fs::create_dir_all(plist_dirpath).map_err(|e| E::DirCreation { 281 | path: plist_dirpath.to_owned(), 282 | source: e, 283 | })?; 284 | } 285 | 286 | write_plist(plist_path_exists, &plist_path, &plist_value)?; 287 | trace!("Plist updated at {plist_path}"); 288 | 289 | Ok(values_changed) 290 | } 291 | 292 | /// Write a plist file to a path. Will fall back to trying to use sudo if a normal write fails. 293 | fn write_plist(plist_path_exists: bool, plist_path: &Utf8Path, plist_value: &plist::Value) -> Result<(), E> { 294 | // 295 | let should_write_binary = !plist_path_exists || is_binary(plist_path)?; 296 | 297 | let write_result = if should_write_binary { 298 | trace!("Writing binary plist"); 299 | plist::to_file_binary(plist_path, &plist_value) 300 | } else { 301 | trace!("Writing xml plist"); 302 | plist::to_file_xml(plist_path, &plist_value) 303 | }; 304 | 305 | let Err(plist_error) = write_result else { 306 | return Ok(()); 307 | }; 308 | 309 | let io_error = match plist_error.into_io() { 310 | Ok(io_error) => io_error, 311 | Err(plist_error) => { 312 | return Err(E::PlistWrite { 313 | path: plist_path.to_path_buf(), 314 | source: plist_error, 315 | }) 316 | } 317 | }; 318 | 319 | trace!("Tried to write plist file, got IO error {io_error:?}, trying again with sudo"); 320 | 321 | let mut plist_bytes = Vec::new(); 322 | 323 | if should_write_binary { 324 | plist::to_writer_binary(&mut plist_bytes, &plist_value) 325 | } else { 326 | plist::to_writer_xml(&mut plist_bytes, &plist_value) 327 | } 328 | .map_err(|e| E::PlistWrite { 329 | path: Utf8Path::new("/dev/stdout").to_path_buf(), 330 | source: e, 331 | })?; 332 | 333 | cmd!("sudo", "tee", plist_path) 334 | .stdin_bytes(plist_bytes) 335 | .stdout_null() 336 | .run() 337 | .map_err(|e| E::PlistSudoWrite { 338 | path: plist_path.to_path_buf(), 339 | source: e, 340 | }) 341 | .map(|_| ())?; 342 | Ok(()) 343 | } 344 | 345 | /// Combines plist values using the following operations: 346 | /// * Merges dictionaries so new keys apply and old keys are let untouched 347 | /// * Replaces "..." in arrays with a copy of the old array (duplicates removed) 348 | /// 349 | /// This operation is performed recursively on dictionaries. 350 | fn merge_value(new_value: &mut Value, old_value: Option<&Value>) { 351 | deep_merge_dictionaries(new_value, old_value); 352 | replace_ellipsis_array(new_value, old_value); 353 | } 354 | 355 | /// Replace `...` values in an input array. 356 | /// You end up with: [, , ] 357 | /// But any duplicates between old and new values are removed, with the first value taking 358 | /// precedence. 359 | fn replace_ellipsis_array(new_value: &mut Value, old_value: Option<&Value>) { 360 | // 361 | let Value::Array(new_array) = new_value else { 362 | trace!("Value isn't an array, skipping ellipsis replacement..."); 363 | return; 364 | }; 365 | 366 | let ellipsis = plist::Value::from(ELLIPSIS); 367 | 368 | let Some(position) = new_array.iter().position(|x| x == &ellipsis) else { 369 | trace!("New value doesn't contain ellipsis, skipping ellipsis replacement..."); 370 | return; 371 | }; 372 | 373 | let Some(old_array) = old_value.and_then(plist::Value::as_array) else { 374 | trace!("Old value wasn't an array, skipping ellipsis replacement..."); 375 | new_array.remove(position); 376 | return; 377 | }; 378 | 379 | let array_copy: Vec<_> = std::mem::take(new_array); 380 | 381 | trace!("Performing array ellipsis replacement..."); 382 | 383 | for element in array_copy { 384 | if element == ellipsis { 385 | for old_element in old_array { 386 | if new_array.contains(old_element) { 387 | continue; 388 | } 389 | new_array.push(old_element.clone()); 390 | } 391 | } else if !new_array.contains(&element) { 392 | new_array.push(element); 393 | } 394 | } 395 | } 396 | 397 | // Recursively merge dictionaries, unless the new value is empty `{}`. 398 | // If a dictionary 399 | // * is empty `{}` 400 | // * contains a key `{}` 401 | // Then the merge step will be skipped for it and its children. 402 | fn deep_merge_dictionaries(new_value: &mut Value, old_value: Option<&Value>) { 403 | // 404 | let Value::Dictionary(new_dict) = new_value else { 405 | trace!("New value is not a dictionary, Skipping merge..."); 406 | return; 407 | }; 408 | 409 | if new_dict.is_empty() { 410 | trace!("New value is an empty dictionary. Skipping merge..."); 411 | return; 412 | } 413 | 414 | // the "..." key is no longer used, and its merging behavior is performed by default. ignore it, for compatibility with older YAML. 415 | new_dict.remove(ELLIPSIS); 416 | 417 | let Some(old_dict) = old_value.and_then(plist::Value::as_dictionary) else { 418 | trace!("Old value wasn't a dict. Skipping merge..."); 419 | return; 420 | }; 421 | 422 | // for each value, recursively invoke this to merge any child dictionaries. 423 | // also perform array ellipsis replacement. 424 | // this occurs even if "!" is present. 425 | for (key, new_child_value) in &mut *new_dict { 426 | let old_child_value = old_dict.get(key); 427 | merge_value(new_child_value, old_child_value); 428 | } 429 | 430 | if new_dict.contains_key(BANG) { 431 | trace!("Dictionary contains key '!'. Skipping merge..."); 432 | new_dict.remove(BANG); 433 | return; 434 | } 435 | 436 | trace!("Performing deep merge..."); 437 | 438 | for (key, old_value) in old_dict { 439 | if !new_dict.contains_key(key) { 440 | new_dict.insert(key.clone(), old_value.clone()); 441 | } 442 | } 443 | } 444 | 445 | /// Get the hardware UUID of the current Mac. 446 | /// You can get the Hardware UUID from: 447 | /// 448 | fn get_hardware_uuid() -> Result { 449 | let raw_output = cmd!("ioreg", "-d2", "-a", "-c", "IOPlatformExpertDevice").read()?; 450 | let ioreg_output: IoregOutput = plist::from_bytes(raw_output.as_bytes())?; 451 | Ok(ioreg_output 452 | .io_registry_entry_children 453 | .into_iter() 454 | .next() 455 | .ok_or_else(|| eyre!("Failed to get the Hardware UUID for the current Mac."))? 456 | .io_platform_uuid) 457 | } 458 | 459 | /// XML output returned by `ioreg -d2 -a -c IOPlatformExpertDevice` 460 | #[derive(Debug, Clone, Deserialize, Serialize)] 461 | struct IoregOutput { 462 | /// The set of `IORegistry` entries. 463 | #[serde(rename = "IORegistryEntryChildren")] 464 | io_registry_entry_children: Vec, 465 | } 466 | 467 | /// A specific `IORegistry` entry. 468 | #[derive(Debug, Clone, Deserialize, Serialize)] 469 | struct IoRegistryEntryChildren { 470 | /// The platform UUID. 471 | #[serde(rename = "IOPlatformUUID")] 472 | io_platform_uuid: String, 473 | } 474 | 475 | /// Helper to allow serializing plists containing binary data to yaml. 476 | /// Replace binary data attributes to work around . 477 | pub fn replace_data_in_plist(value: &mut Value) -> Result<()> { 478 | let mut stringified_data_value = match value { 479 | Value::Array(arr) => { 480 | for el in arr.iter_mut() { 481 | replace_data_in_plist(el)?; 482 | } 483 | return Ok(()); 484 | } 485 | Value::Dictionary(dict) => { 486 | for (_, v) in dict.iter_mut() { 487 | replace_data_in_plist(v)?; 488 | } 489 | return Ok(()); 490 | } 491 | Value::Data(bytes) => Value::String(hex::encode(bytes)), 492 | _ => { 493 | return Ok(()); 494 | } 495 | }; 496 | mem::swap(value, &mut stringified_data_value); 497 | 498 | Ok(()) 499 | } 500 | 501 | #[cfg(test)] 502 | mod tests { 503 | use log::info; 504 | use testresult::TestResult; 505 | 506 | use crate::defaults::deep_merge_dictionaries; 507 | 508 | use super::{replace_ellipsis_array, NS_GLOBAL_DOMAIN}; 509 | 510 | #[test] 511 | fn plist_path_tests() -> TestResult { 512 | let home_dir = dirs::home_dir().expect("Expected to be able to calculate the user's home directory."); 513 | 514 | { 515 | let domain_path = super::plist_path(NS_GLOBAL_DOMAIN, false)?; 516 | assert_eq!(home_dir.join("Library/Preferences/.GlobalPreferences.plist"), domain_path); 517 | } 518 | 519 | { 520 | let mut expected_plist_path = home_dir.join( 521 | "Library/Containers/com.apple.Safari/Data/Library/Preferences/com.apple.Safari.\ 522 | plist", 523 | ); 524 | if !expected_plist_path.exists() { 525 | expected_plist_path = home_dir.join("Library/Preferences/com.apple.Safari.plist"); 526 | } 527 | let domain_path = super::plist_path("com.apple.Safari", false)?; 528 | assert_eq!(expected_plist_path, domain_path); 529 | } 530 | 531 | // Per-host preference (`current_host` is true). 532 | { 533 | let domain_path = super::plist_path(NS_GLOBAL_DOMAIN, true)?; 534 | let hardware_uuid = super::get_hardware_uuid()?; 535 | assert_eq!( 536 | home_dir.join(format!("Library/Preferences/ByHost/.GlobalPreferences.{hardware_uuid}.plist")), 537 | domain_path 538 | ); 539 | } 540 | 541 | // Per-host sandboxed preference (`current_host` is true and the sandboxed plist exists). 542 | { 543 | let domain_path = super::plist_path("com.apple.Safari", true)?; 544 | let hardware_uuid = super::get_hardware_uuid()?; 545 | assert_eq!( 546 | home_dir.join(format!( 547 | "Library/Containers/com.apple.Safari/Data/Library/Preferences/ByHost/com.\ 548 | apple.Safari.{hardware_uuid}.plist" 549 | )), 550 | domain_path 551 | ); 552 | } 553 | 554 | Ok(()) 555 | } 556 | 557 | #[test] 558 | fn test_get_hardware_uuid() -> TestResult { 559 | use duct::cmd; 560 | 561 | let system_profiler_output = cmd!("system_profiler", "SPHardwareDataType").read()?; 562 | 563 | let expected_value = system_profiler_output 564 | .lines() 565 | .find_map(|line| line.contains("UUID").then(|| line.split_whitespace().last().unwrap_or_default())) 566 | .unwrap_or_default(); 567 | 568 | let actual_value = super::get_hardware_uuid()?; 569 | assert_eq!(expected_value, actual_value); 570 | 571 | Ok(()) 572 | } 573 | 574 | #[test] 575 | fn test_serialize_binary() -> TestResult { 576 | // Modified version of ~/Library/Preferences/com.apple.humanunderstanding.plist 577 | let binary_plist_as_hex = "62706c6973743030d101025f10124861736847656e657261746f722e73616c744f10201111111122222222333333334444444455555555666666667777777788888888080b200000000000000101000000000000000300000000000000000000000000000043"; 578 | let expected_yaml = "HashGenerator.salt: \ 579 | '1111111122222222333333334444444455555555666666667777777788888888'\n"; 580 | 581 | let binary_plist = hex::decode(binary_plist_as_hex)?; 582 | 583 | let mut value: plist::Value = plist::from_bytes(&binary_plist)?; 584 | info!("Value before: {value:?}"); 585 | super::replace_data_in_plist(&mut value)?; 586 | info!("Value after: {value:?}"); 587 | let yaml_string = serde_yaml::to_string(&value)?; 588 | info!("Yaml value: {yaml_string}"); 589 | assert_eq!(expected_yaml, yaml_string); 590 | 591 | Ok(()) 592 | } 593 | 594 | #[test] 595 | fn test_deep_merge_dictionaries() { 596 | use plist::{Dictionary, Value}; 597 | 598 | let old_value = Dictionary::from_iter([ 599 | ("foo", Value::from(10)), // !!! takes precedence 600 | ("fub", 11.into()), // !!! 601 | ("bar", 12.into()), // ! 602 | ("baz", 13.into()), // ! 603 | ]) 604 | .into(); 605 | let mut new_value = Dictionary::from_iter([ 606 | ("bar", Value::from(22)), // !! 607 | ("baz", 23.into()), // !! takes precedence 608 | ]) 609 | .into(); 610 | 611 | deep_merge_dictionaries(&mut new_value, Some(&old_value)); 612 | 613 | let expected = Dictionary::from_iter([ 614 | ("foo", Value::from(10)), // from new 615 | ("fub", 11.into()), 616 | ("bar", 22.into()), // from old 617 | ("baz", 23.into()), 618 | ]) 619 | .into(); 620 | 621 | assert_eq!(new_value, expected); 622 | } 623 | 624 | #[test] 625 | fn test_replace_ellipsis_dict_nested() { 626 | use plist::{Dictionary, Value}; 627 | 628 | let old_value = Dictionary::from_iter([( 629 | "level_1", 630 | Dictionary::from_iter([( 631 | "level_2", 632 | Dictionary::from_iter([ 633 | ("foo", Value::from(10)), // 634 | ("bar", 20.into()), 635 | ("baz", 30.into()), 636 | ]), 637 | )]), 638 | )]) 639 | .into(); 640 | 641 | let mut new_value = Dictionary::from_iter([( 642 | "level_1", 643 | Dictionary::from_iter([( 644 | "level_2", 645 | Dictionary::from_iter([ 646 | ("baz", Value::from(90)), // 647 | ]), 648 | )]), 649 | )]) 650 | .into(); 651 | 652 | deep_merge_dictionaries(&mut new_value, Some(&old_value)); 653 | 654 | let expected = Dictionary::from_iter([( 655 | "level_1", 656 | Dictionary::from_iter([( 657 | "level_2", 658 | Dictionary::from_iter([ 659 | ("foo", Value::from(10)), // 660 | ("bar", 20.into()), 661 | ("baz", 90.into()), 662 | ]), 663 | )]), 664 | )]) 665 | .into(); 666 | 667 | assert_eq!(new_value, expected); 668 | } 669 | 670 | #[test] 671 | fn test_replace_ellipsis_dict_nested_bang() { 672 | use plist::{Dictionary, Value}; 673 | 674 | let old_value = Dictionary::from_iter([( 675 | "level_1", 676 | Dictionary::from_iter([( 677 | "level_2", 678 | Dictionary::from_iter([ 679 | ("foo", Value::from(10)), // 680 | ("bar", 20.into()), 681 | ("baz", 30.into()), 682 | ]), 683 | )]), 684 | )]) 685 | .into(); 686 | 687 | let mut new_value = Dictionary::from_iter([( 688 | "level_1", 689 | Dictionary::from_iter([( 690 | "level_2", 691 | Dictionary::from_iter([ 692 | ("!", Value::from("")), // 693 | ("baz", 90.into()), // 694 | ]), 695 | )]), 696 | )]) 697 | .into(); 698 | 699 | deep_merge_dictionaries(&mut new_value, Some(&old_value)); 700 | 701 | let expected = Dictionary::from_iter([( 702 | "level_1", 703 | Dictionary::from_iter([("level_2", Dictionary::from_iter([("baz", Value::from(90))]))]), 704 | )]) 705 | .into(); 706 | 707 | assert_eq!(new_value, expected); 708 | } 709 | 710 | #[test] 711 | fn test_replace_ellipsis_array() { 712 | let old_value = vec![ 713 | 10.into(), // ! 714 | 20.into(), // ! 715 | 30.into(), // ! 716 | 40.into(), // ! 717 | ] 718 | .into(); 719 | let mut new_value = vec![ 720 | 30.into(), // !!! 721 | 20.into(), // !!! 722 | "...".into(), 723 | 60.into(), // !! 724 | 50.into(), // !! 725 | 40.into(), // !! 726 | ] 727 | .into(); 728 | 729 | replace_ellipsis_array(&mut new_value, Some(&old_value)); 730 | 731 | let expected = vec![ 732 | 30.into(), // from new array before "..." 733 | 20.into(), 734 | 10.into(), // from old array 735 | 40.into(), 736 | 60.into(), // from new array after "..." 737 | 50.into(), 738 | ] 739 | .into(); 740 | 741 | assert_eq!(new_value, expected); 742 | } 743 | } 744 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | // NB: Most of this code originated from: https://github.com/gibfahn/up-rs, MIT & Apache 2.0 licensed. 2 | 3 | use camino::Utf8PathBuf; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum DefaultsError { 8 | #[error("Unable to create dir at: {path}.")] 9 | DirCreation { path: Utf8PathBuf, source: std::io::Error }, 10 | 11 | #[error("Unable to copy file. From: {from_path} To: {to_path}")] 12 | FileCopy { 13 | from_path: Utf8PathBuf, 14 | to_path: Utf8PathBuf, 15 | source: std::io::Error, 16 | }, 17 | 18 | #[error("Failed to read bytes from path {path}")] 19 | FileRead { path: Utf8PathBuf, source: std::io::Error }, 20 | 21 | #[error("Expected to find a plist dictionary, but found a {plist_type} instead.\nDomain: {domain:?}\nKey: {key:?}")] 22 | NotADictionary { domain: String, key: String, plist_type: &'static str }, 23 | 24 | #[error("Failed to read Plist file {path}.")] 25 | PlistRead { path: Utf8PathBuf, source: plist::Error }, 26 | 27 | #[error("Failed to write value to plist file {path}")] 28 | PlistWrite { path: Utf8PathBuf, source: plist::Error }, 29 | 30 | #[error("Failed to write a value to plist file {path} as sudo.")] 31 | PlistSudoWrite { path: Utf8PathBuf, source: std::io::Error }, 32 | 33 | #[error("Invalid YAML at '{path}'")] 34 | InvalidYaml { path: Utf8PathBuf, source: serde_yaml::Error }, 35 | 36 | #[error("Failed to serialize plist to YAML. Domain: {domain:?}")] 37 | SerializationFailed { domain: String, source: serde_yaml::Error }, 38 | 39 | #[error("Failed to deserialize the YAML file or string.")] 40 | DeserializationFailed { source: serde_yaml::Error }, 41 | 42 | #[error("Expected a domain, but didn't find one.")] 43 | MissingDomain {}, 44 | 45 | #[error("Unexpectedly empty option found.")] 46 | UnexpectedNone, 47 | 48 | #[error("Eyre error.")] 49 | EyreError { source: color_eyre::Report }, 50 | 51 | #[error("failed to split YAML file {path}")] 52 | YamlSplitError { path: Utf8PathBuf, source: yaml_split::YamlSplitError }, 53 | } 54 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all, clippy::pedantic, clippy::unwrap_used)] 2 | #![allow( 3 | clippy::module_name_repetitions, 4 | clippy::missing_errors_doc, 5 | clippy::missing_panics_doc, 6 | 7 | // Ignore clippy for the generated file from shadow-rs. 8 | // https://github.com/baoyachi/shadow-rs/issues/151 9 | clippy::non_ascii_literal, 10 | clippy::print_stdout, 11 | clippy::needless_raw_strings, 12 | clippy::needless_raw_string_hashes 13 | )] 14 | 15 | use std::fs; 16 | use std::io; 17 | 18 | use camino::Utf8PathBuf; 19 | use clap::crate_authors; 20 | use clap::{ArgGroup, CommandFactory, Parser, Subcommand, ValueHint}; 21 | use clap_complete::{generate, Shell as CompletionShell}; 22 | use color_eyre::eyre::Result; 23 | use shadow_rs::shadow; 24 | 25 | // https://crates.io/crates/shadow-rs 26 | shadow!(build); 27 | 28 | mod cmd; 29 | mod defaults; 30 | mod errors; 31 | 32 | use self::cmd::{apply_defaults, dump, process_path}; 33 | use crate::errors::DefaultsError as E; 34 | 35 | #[derive(Parser, Debug)] 36 | #[clap( 37 | author=crate_authors!(), 38 | version=build::PKG_VERSION, 39 | long_version=build::CLAP_LONG_VERSION, 40 | about="Generate and apply macOS defaults.", 41 | subcommand_required=true, 42 | arg_required_else_help=true, 43 | )] 44 | #[allow(clippy::upper_case_acronyms)] 45 | struct CLI { 46 | /// Don’t actually run anything. 47 | #[arg(short, long)] 48 | dry_run: bool, 49 | 50 | #[clap(flatten)] 51 | verbose: clap_verbosity_flag::Verbosity, 52 | 53 | /// Clap subcommand to run. 54 | #[clap(subcommand)] 55 | command: Commands, 56 | } 57 | 58 | #[derive(Debug, Subcommand)] 59 | pub(crate) enum Commands { 60 | /// Set macOS defaults in plist files. 61 | Apply { 62 | /// Sets the input file or path to use. 63 | #[arg(required = true, value_hint = ValueHint::FilePath)] 64 | path: Utf8PathBuf, 65 | 66 | /// If changes were applied, exit with this return code. 67 | #[clap(short, long, default_value = "0")] 68 | exit_code: i32, 69 | }, 70 | 71 | /// Generate shell completions to stdout. 72 | Completions { 73 | #[clap(value_enum)] 74 | shell: CompletionShell, 75 | }, 76 | 77 | /// Dump existing defaults as YAML. 78 | #[clap(group( 79 | ArgGroup::new("dump") 80 | .required(true) 81 | .args(&["domain", "global_domain"]), 82 | ))] 83 | Dump { 84 | /// Read from the current host. 85 | #[arg(short, long)] 86 | current_host: bool, 87 | 88 | /// Read from the global domain. 89 | #[clap(short, long)] 90 | global_domain: bool, 91 | 92 | /// Domain to generate. 93 | #[clap(short, long)] 94 | domain: Option, 95 | 96 | /// Path to YAML file for dump output. 97 | #[arg(value_hint = ValueHint::FilePath)] 98 | path: Option, 99 | }, 100 | } 101 | 102 | fn main() -> Result<()> { 103 | color_eyre::install()?; 104 | 105 | let cli = CLI::parse(); 106 | 107 | env_logger::Builder::new().filter_level(cli.verbose.log_level_filter()).init(); 108 | 109 | match cli.command { 110 | Commands::Apply { path, exit_code } => { 111 | // 112 | let mut changed = false; 113 | 114 | for p in process_path(path)? { 115 | fs::metadata(&p).map_err(|e| E::FileRead { path: p.clone(), source: e })?; 116 | 117 | if apply_defaults(&p)? { 118 | changed = true; 119 | } 120 | } 121 | 122 | std::process::exit(if changed { exit_code } else { 0 }); 123 | } 124 | Commands::Completions { shell } => { 125 | generate(shell, &mut CLI::command(), "macos-defaults", &mut io::stdout().lock()); 126 | Ok(()) 127 | } 128 | Commands::Dump { 129 | current_host, 130 | path, 131 | global_domain, 132 | domain, 133 | } => dump(current_host, path, global_domain, domain), 134 | }?; 135 | 136 | std::process::exit(0); 137 | } 138 | --------------------------------------------------------------------------------