├── .editorconfig ├── .github └── workflows │ └── ddcset.yml ├── .gitignore ├── .rustfmt.toml ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── README.md ├── ci.nix ├── default.nix ├── derivation.nix ├── eudev-gettid.patch ├── flake.lock ├── flake.nix ├── lock.nix ├── shell.nix └── src ├── capabilities.rs ├── detect.rs ├── getvcp.rs ├── lib.rs ├── main.rs ├── save.rs ├── setvcp.rs └── util.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | 8 | [*.rs] 9 | indent_style = tab 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.github/workflows/ddcset.yml: -------------------------------------------------------------------------------- 1 | env: 2 | CI_ALLOW_ROOT: '1' 3 | CI_CONFIG: ./ci.nix 4 | CI_PLATFORM: gh-actions 5 | jobs: 6 | ci-check: 7 | name: ddcset check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - id: checkout 11 | name: git clone 12 | uses: actions/checkout@v1 13 | with: 14 | submodules: true 15 | - id: nix-install 16 | name: nix install 17 | uses: arcnmx/ci/actions/nix/install@master 18 | - id: ci-action-build 19 | name: nix build ci.gh-actions.configFile 20 | uses: arcnmx/ci/actions/nix/build@master 21 | with: 22 | attrs: ci.gh-actions.configFile 23 | out-link: .ci/workflow.yml 24 | - id: ci-action-compare 25 | name: gh-actions compare 26 | uses: arcnmx/ci/actions/nix/run@master 27 | with: 28 | args: -u .github/workflows/ddcset.yml .ci/workflow.yml 29 | attrs: nixpkgs.diffutils 30 | command: diff 31 | macos: 32 | name: ddcset-macos 33 | permissions: 34 | contents: write 35 | runs-on: macos-10.15 36 | steps: 37 | - id: checkout 38 | name: git clone 39 | uses: actions/checkout@v1 40 | with: 41 | submodules: true 42 | - id: nix-install 43 | name: nix install 44 | uses: arcnmx/ci/actions/nix/install@master 45 | - id: ci-setup 46 | name: nix setup 47 | uses: arcnmx/ci/actions/nix/run@master 48 | with: 49 | attrs: ci.job.macos.run.setup 50 | quiet: false 51 | - id: ci-dirty 52 | name: nix test dirty 53 | uses: arcnmx/ci/actions/nix/run@master 54 | with: 55 | attrs: ci.job.macos.run.test 56 | command: ci-build-dirty 57 | quiet: false 58 | stdout: ${{ runner.temp }}/ci.build.dirty 59 | - id: ci-test 60 | name: nix test build 61 | uses: arcnmx/ci/actions/nix/run@master 62 | with: 63 | attrs: ci.job.macos.run.test 64 | command: ci-build-realise 65 | ignore-exit-code: true 66 | quiet: false 67 | stdin: ${{ runner.temp }}/ci.build.dirty 68 | - env: 69 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 70 | id: ci-summary 71 | name: nix test results 72 | uses: arcnmx/ci/actions/nix/run@master 73 | with: 74 | attrs: ci.job.macos.run.test 75 | command: ci-build-summarise 76 | quiet: false 77 | stdin: ${{ runner.temp }}/ci.build.dirty 78 | stdout: ${{ runner.temp }}/ci.build.cache 79 | - env: 80 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 81 | id: ci-cache 82 | if: always() 83 | name: nix test cache 84 | uses: arcnmx/ci/actions/nix/run@master 85 | with: 86 | attrs: ci.job.macos.run.test 87 | command: ci-build-cache 88 | quiet: false 89 | stdin: ${{ runner.temp }}/ci.build.cache 90 | - id: artifact-build 91 | name: artifact build 92 | uses: arcnmx/ci/actions/nix/build@master 93 | with: 94 | attrs: config.jobs.macos.artifactPackage 95 | file: 96 | out-link: .ci/artifacts 97 | - id: artifact-upload 98 | name: artifact upload 99 | uses: actions/upload-artifact@v3 100 | with: 101 | name: ddcset 102 | path: .ci/artifacts/bin/ddcset* 103 | - id: release-upload 104 | if: startsWith(github.ref, 'refs/tags/') 105 | name: release 106 | uses: softprops/action-gh-release@v1 107 | with: 108 | files: .ci/artifacts/bin/ddcset* 109 | nixos: 110 | name: ddcset-nixos 111 | permissions: 112 | contents: write 113 | runs-on: ubuntu-latest 114 | steps: 115 | - id: checkout 116 | name: git clone 117 | uses: actions/checkout@v1 118 | with: 119 | submodules: true 120 | - id: nix-install 121 | name: nix install 122 | uses: arcnmx/ci/actions/nix/install@master 123 | - id: ci-setup 124 | name: nix setup 125 | uses: arcnmx/ci/actions/nix/run@master 126 | with: 127 | attrs: ci.job.nixos.run.setup 128 | quiet: false 129 | - id: ci-dirty 130 | name: nix test dirty 131 | uses: arcnmx/ci/actions/nix/run@master 132 | with: 133 | attrs: ci.job.nixos.run.test 134 | command: ci-build-dirty 135 | quiet: false 136 | stdout: ${{ runner.temp }}/ci.build.dirty 137 | - id: ci-test 138 | name: nix test build 139 | uses: arcnmx/ci/actions/nix/run@master 140 | with: 141 | attrs: ci.job.nixos.run.test 142 | command: ci-build-realise 143 | ignore-exit-code: true 144 | quiet: false 145 | stdin: ${{ runner.temp }}/ci.build.dirty 146 | - env: 147 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 148 | id: ci-summary 149 | name: nix test results 150 | uses: arcnmx/ci/actions/nix/run@master 151 | with: 152 | attrs: ci.job.nixos.run.test 153 | command: ci-build-summarise 154 | quiet: false 155 | stdin: ${{ runner.temp }}/ci.build.dirty 156 | stdout: ${{ runner.temp }}/ci.build.cache 157 | - env: 158 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 159 | id: ci-cache 160 | if: always() 161 | name: nix test cache 162 | uses: arcnmx/ci/actions/nix/run@master 163 | with: 164 | attrs: ci.job.nixos.run.test 165 | command: ci-build-cache 166 | quiet: false 167 | stdin: ${{ runner.temp }}/ci.build.cache 168 | - id: artifact-build 169 | name: artifact build 170 | uses: arcnmx/ci/actions/nix/build@master 171 | with: 172 | attrs: config.jobs.nixos.artifactPackage 173 | file: 174 | out-link: .ci/artifacts 175 | - id: artifact-upload 176 | name: artifact upload 177 | uses: actions/upload-artifact@v3 178 | with: 179 | name: ddcset 180 | path: .ci/artifacts/bin/ddcset* 181 | - id: release-upload 182 | if: startsWith(github.ref, 'refs/tags/') 183 | name: release 184 | uses: softprops/action-gh-release@v1 185 | with: 186 | files: .ci/artifacts/bin/ddcset* 187 | name: ddcset 188 | 'on': 189 | - push 190 | - pull_request 191 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.cargo/ 3 | /result* 4 | *.swp 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | comment_width = 100 2 | condense_wildcard_suffixes = true 3 | hard_tabs = true 4 | imports_granularity = "One" 5 | group_imports = "One" 6 | match_arm_blocks = false 7 | match_block_trailing_comma = true 8 | force_multiline_blocks = false 9 | max_width = 120 10 | newline_style = "Unix" 11 | normalize_comments = false 12 | overflow_delimited_expr = true 13 | reorder_impl_items = true 14 | reorder_modules = true 15 | tab_spaces = 2 16 | trailing_semicolon = false 17 | unstable_features = true 18 | use_field_init_shorthand = true 19 | use_try_shorthand = true 20 | wrap_comments = true 21 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 arcnmx 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr 2.5.0", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.67" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "7724808837b77f4b4de9d283820f9d98bcf496d5692934b857a2399d31ff22e6" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 25 | 26 | [[package]] 27 | name = "bitflags" 28 | version = "1.3.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 31 | 32 | [[package]] 33 | name = "byteorder" 34 | version = "1.4.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 37 | 38 | [[package]] 39 | name = "cc" 40 | version = "1.0.78" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "clap" 52 | version = "4.0.29" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" 55 | dependencies = [ 56 | "bitflags", 57 | "clap_derive", 58 | "clap_lex", 59 | "is-terminal", 60 | "once_cell", 61 | "strsim", 62 | "termcolor", 63 | ] 64 | 65 | [[package]] 66 | name = "clap_derive" 67 | version = "4.0.21" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" 70 | dependencies = [ 71 | "heck", 72 | "proc-macro-error", 73 | "proc-macro2", 74 | "quote", 75 | "syn", 76 | ] 77 | 78 | [[package]] 79 | name = "clap_lex" 80 | version = "0.3.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" 83 | dependencies = [ 84 | "os_str_bytes", 85 | ] 86 | 87 | [[package]] 88 | name = "core-foundation" 89 | version = "0.9.3" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 92 | dependencies = [ 93 | "core-foundation-sys", 94 | "libc", 95 | ] 96 | 97 | [[package]] 98 | name = "core-foundation-sys" 99 | version = "0.8.3" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 102 | 103 | [[package]] 104 | name = "core-graphics" 105 | version = "0.22.3" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" 108 | dependencies = [ 109 | "bitflags", 110 | "core-foundation", 111 | "core-graphics-types", 112 | "foreign-types", 113 | "libc", 114 | ] 115 | 116 | [[package]] 117 | name = "core-graphics-types" 118 | version = "0.1.1" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" 121 | dependencies = [ 122 | "bitflags", 123 | "core-foundation", 124 | "foreign-types", 125 | "libc", 126 | ] 127 | 128 | [[package]] 129 | name = "ctor" 130 | version = "0.1.26" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" 133 | dependencies = [ 134 | "quote", 135 | "syn", 136 | ] 137 | 138 | [[package]] 139 | name = "ddc" 140 | version = "0.3.0" 141 | source = "git+https://github.com/arcnmx/ddc-rs?branch=v0.3.x#fbbff56372a957bed86a99cd6bc4717a7c433397" 142 | dependencies = [ 143 | "mccs", 144 | ] 145 | 146 | [[package]] 147 | name = "ddc-hi" 148 | version = "0.5.0" 149 | source = "git+https://github.com/arcnmx/ddc-hi-rs?branch=v0.5.x#a64a97177bf9014f12c34c73a01c637247f3a738" 150 | dependencies = [ 151 | "ddc", 152 | "ddc-i2c", 153 | "ddc-macos", 154 | "ddc-winapi", 155 | "edid", 156 | "log", 157 | "mccs", 158 | "mccs-caps", 159 | "mccs-db", 160 | "nvapi", 161 | "thiserror", 162 | ] 163 | 164 | [[package]] 165 | name = "ddc-i2c" 166 | version = "0.3.0" 167 | source = "git+https://github.com/arcnmx/ddc-i2c-rs?branch=v0.3.x#8e7dd22fc2679c71fbdfab67f4067be3ffeb0afa" 168 | dependencies = [ 169 | "ddc", 170 | "i2c", 171 | "i2c-linux", 172 | ] 173 | 174 | [[package]] 175 | name = "ddc-macos" 176 | version = "0.3.0" 177 | source = "git+https://github.com/arcnmx/ddc-macos-rs?branch=v0.3.x#fdc7fdaf838b36e37dbf983f19646ed6d38f58b0" 178 | dependencies = [ 179 | "core-foundation", 180 | "core-foundation-sys", 181 | "core-graphics", 182 | "ddc", 183 | "io-kit-sys", 184 | "mach", 185 | "thiserror", 186 | ] 187 | 188 | [[package]] 189 | name = "ddc-winapi" 190 | version = "0.3.0" 191 | source = "git+https://github.com/arcnmx/ddc-winapi-rs?branch=v0.3.x#2b658281bbcf665f953a87cac34aed11d77878b6" 192 | dependencies = [ 193 | "bitflags", 194 | "ddc", 195 | "log", 196 | "widestring", 197 | "windows", 198 | ] 199 | 200 | [[package]] 201 | name = "ddcset" 202 | version = "0.1.0" 203 | dependencies = [ 204 | "anyhow", 205 | "clap", 206 | "ddc-hi", 207 | "env_logger", 208 | "hex", 209 | "log", 210 | "mccs-db", 211 | "once_cell", 212 | ] 213 | 214 | [[package]] 215 | name = "edid" 216 | version = "0.3.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "24ce75530893d834dcfe3bb67ce0e7dec489484e7cb4423ca31618af4bab24fe" 219 | dependencies = [ 220 | "nom 3.2.1", 221 | ] 222 | 223 | [[package]] 224 | name = "env_logger" 225 | version = "0.10.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 228 | dependencies = [ 229 | "humantime", 230 | "is-terminal", 231 | "log", 232 | "regex", 233 | "termcolor", 234 | ] 235 | 236 | [[package]] 237 | name = "errno" 238 | version = "0.2.8" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 241 | dependencies = [ 242 | "errno-dragonfly", 243 | "libc", 244 | "winapi", 245 | ] 246 | 247 | [[package]] 248 | name = "errno-dragonfly" 249 | version = "0.1.2" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 252 | dependencies = [ 253 | "cc", 254 | "libc", 255 | ] 256 | 257 | [[package]] 258 | name = "foreign-types" 259 | version = "0.3.2" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 262 | dependencies = [ 263 | "foreign-types-shared", 264 | ] 265 | 266 | [[package]] 267 | name = "foreign-types-shared" 268 | version = "0.1.1" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 271 | 272 | [[package]] 273 | name = "hashbrown" 274 | version = "0.12.3" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 277 | 278 | [[package]] 279 | name = "heck" 280 | version = "0.4.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 283 | 284 | [[package]] 285 | name = "hermit-abi" 286 | version = "0.2.6" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 289 | dependencies = [ 290 | "libc", 291 | ] 292 | 293 | [[package]] 294 | name = "hex" 295 | version = "0.4.3" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 298 | 299 | [[package]] 300 | name = "humantime" 301 | version = "2.1.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 304 | 305 | [[package]] 306 | name = "i2c" 307 | version = "0.1.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "60c7b7bdd7b3a985fdcf94a0d7d98e7a47fde8b7f22fb55ce1a91cc104a2ce9a" 310 | dependencies = [ 311 | "bitflags", 312 | ] 313 | 314 | [[package]] 315 | name = "i2c-linux" 316 | version = "0.2.0" 317 | source = "git+https://github.com/arcnmx/i2c-linux-rs?branch=v0.2.x#f0aea3cd6b6fa825960cf0f765c53f92ff616e25" 318 | dependencies = [ 319 | "bitflags", 320 | "i2c", 321 | "i2c-linux-sys", 322 | "resize-slice", 323 | "udev", 324 | ] 325 | 326 | [[package]] 327 | name = "i2c-linux-sys" 328 | version = "0.2.1" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "55cd060ed0016621d3da4ed3a23b0158084de90d1f3a8e59f3d391aacd3bbcf8" 331 | dependencies = [ 332 | "bitflags", 333 | "byteorder", 334 | "libc", 335 | ] 336 | 337 | [[package]] 338 | name = "indexmap" 339 | version = "1.9.2" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 342 | dependencies = [ 343 | "autocfg", 344 | "hashbrown", 345 | ] 346 | 347 | [[package]] 348 | name = "io-kit-sys" 349 | version = "0.2.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "7789f7f3c9686f96164f5109d69152de759e76e284f736bd57661c6df5091919" 352 | dependencies = [ 353 | "core-foundation-sys", 354 | "mach", 355 | ] 356 | 357 | [[package]] 358 | name = "io-lifetimes" 359 | version = "1.0.3" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 362 | dependencies = [ 363 | "libc", 364 | "windows-sys", 365 | ] 366 | 367 | [[package]] 368 | name = "is-terminal" 369 | version = "0.4.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" 372 | dependencies = [ 373 | "hermit-abi", 374 | "io-lifetimes", 375 | "rustix", 376 | "windows-sys", 377 | ] 378 | 379 | [[package]] 380 | name = "itoa" 381 | version = "1.0.5" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 384 | 385 | [[package]] 386 | name = "libc" 387 | version = "0.2.138" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 390 | 391 | [[package]] 392 | name = "libudev-sys" 393 | version = "0.1.4" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" 396 | dependencies = [ 397 | "libc", 398 | "pkg-config", 399 | ] 400 | 401 | [[package]] 402 | name = "linux-raw-sys" 403 | version = "0.1.4" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 406 | 407 | [[package]] 408 | name = "log" 409 | version = "0.4.17" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 412 | dependencies = [ 413 | "cfg-if", 414 | "value-bag", 415 | ] 416 | 417 | [[package]] 418 | name = "mach" 419 | version = "0.3.2" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" 422 | dependencies = [ 423 | "libc", 424 | ] 425 | 426 | [[package]] 427 | name = "mccs" 428 | version = "0.2.0" 429 | source = "git+https://github.com/arcnmx/mccs-rs?branch=v0.2.x#e2be7229140632b04f4252d6e779fcfe90b6046e" 430 | 431 | [[package]] 432 | name = "mccs-caps" 433 | version = "0.2.0" 434 | source = "git+https://github.com/arcnmx/mccs-rs?branch=v0.2.x#e2be7229140632b04f4252d6e779fcfe90b6046e" 435 | dependencies = [ 436 | "mccs", 437 | "nom 7.1.1", 438 | ] 439 | 440 | [[package]] 441 | name = "mccs-db" 442 | version = "0.2.0" 443 | source = "git+https://github.com/arcnmx/mccs-rs?branch=v0.2.x#e2be7229140632b04f4252d6e779fcfe90b6046e" 444 | dependencies = [ 445 | "mccs", 446 | "nom 7.1.1", 447 | "serde", 448 | "serde_yaml", 449 | ] 450 | 451 | [[package]] 452 | name = "memchr" 453 | version = "1.0.2" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" 456 | dependencies = [ 457 | "libc", 458 | ] 459 | 460 | [[package]] 461 | name = "memchr" 462 | version = "2.5.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 465 | 466 | [[package]] 467 | name = "minimal-lexical" 468 | version = "0.2.1" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 471 | 472 | [[package]] 473 | name = "nom" 474 | version = "3.2.1" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" 477 | dependencies = [ 478 | "memchr 1.0.2", 479 | ] 480 | 481 | [[package]] 482 | name = "nom" 483 | version = "7.1.1" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 486 | dependencies = [ 487 | "memchr 2.5.0", 488 | "minimal-lexical", 489 | ] 490 | 491 | [[package]] 492 | name = "nvapi" 493 | version = "0.1.4" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "7c63de8cd8362e2c38d1a48dea6ae68e6293a8d8d22a52180d0f8dcc779b3158" 496 | dependencies = [ 497 | "i2c", 498 | "log", 499 | "nvapi-sys", 500 | "void", 501 | ] 502 | 503 | [[package]] 504 | name = "nvapi-sys" 505 | version = "0.1.3" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "b29e9a9393c69ee856bfcf5f76ed1ef32d2c0dd6f58558fd43334278fc1e7ea7" 508 | dependencies = [ 509 | "bitflags", 510 | "winapi", 511 | ] 512 | 513 | [[package]] 514 | name = "once_cell" 515 | version = "1.16.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 518 | 519 | [[package]] 520 | name = "os_str_bytes" 521 | version = "6.4.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 524 | 525 | [[package]] 526 | name = "pkg-config" 527 | version = "0.3.26" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 530 | 531 | [[package]] 532 | name = "proc-macro-error" 533 | version = "1.0.4" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 536 | dependencies = [ 537 | "proc-macro-error-attr", 538 | "proc-macro2", 539 | "quote", 540 | "syn", 541 | "version_check", 542 | ] 543 | 544 | [[package]] 545 | name = "proc-macro-error-attr" 546 | version = "1.0.4" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 549 | dependencies = [ 550 | "proc-macro2", 551 | "quote", 552 | "version_check", 553 | ] 554 | 555 | [[package]] 556 | name = "proc-macro2" 557 | version = "1.0.47" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 560 | dependencies = [ 561 | "unicode-ident", 562 | ] 563 | 564 | [[package]] 565 | name = "quote" 566 | version = "1.0.21" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 569 | dependencies = [ 570 | "proc-macro2", 571 | ] 572 | 573 | [[package]] 574 | name = "regex" 575 | version = "1.7.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 578 | dependencies = [ 579 | "aho-corasick", 580 | "memchr 2.5.0", 581 | "regex-syntax", 582 | ] 583 | 584 | [[package]] 585 | name = "regex-syntax" 586 | version = "0.6.28" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 589 | 590 | [[package]] 591 | name = "resize-slice" 592 | version = "0.1.3" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "8a3cb2f74a9891e76958b9e0ccd269a25b466c3ae3bb3efd71db157248308c4a" 595 | dependencies = [ 596 | "uninitialized", 597 | ] 598 | 599 | [[package]] 600 | name = "rustix" 601 | version = "0.36.5" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" 604 | dependencies = [ 605 | "bitflags", 606 | "errno", 607 | "io-lifetimes", 608 | "libc", 609 | "linux-raw-sys", 610 | "windows-sys", 611 | ] 612 | 613 | [[package]] 614 | name = "ryu" 615 | version = "1.0.11" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 618 | 619 | [[package]] 620 | name = "serde" 621 | version = "1.0.151" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" 624 | dependencies = [ 625 | "serde_derive", 626 | ] 627 | 628 | [[package]] 629 | name = "serde_derive" 630 | version = "1.0.151" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" 633 | dependencies = [ 634 | "proc-macro2", 635 | "quote", 636 | "syn", 637 | ] 638 | 639 | [[package]] 640 | name = "serde_yaml" 641 | version = "0.9.14" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da" 644 | dependencies = [ 645 | "indexmap", 646 | "itoa", 647 | "ryu", 648 | "serde", 649 | "unsafe-libyaml", 650 | ] 651 | 652 | [[package]] 653 | name = "strsim" 654 | version = "0.10.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 657 | 658 | [[package]] 659 | name = "sval" 660 | version = "1.0.0-alpha.5" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" 663 | 664 | [[package]] 665 | name = "syn" 666 | version = "1.0.105" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 669 | dependencies = [ 670 | "proc-macro2", 671 | "quote", 672 | "unicode-ident", 673 | ] 674 | 675 | [[package]] 676 | name = "termcolor" 677 | version = "1.1.3" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 680 | dependencies = [ 681 | "winapi-util", 682 | ] 683 | 684 | [[package]] 685 | name = "thiserror" 686 | version = "1.0.37" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 689 | dependencies = [ 690 | "thiserror-impl", 691 | ] 692 | 693 | [[package]] 694 | name = "thiserror-impl" 695 | version = "1.0.37" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 698 | dependencies = [ 699 | "proc-macro2", 700 | "quote", 701 | "syn", 702 | ] 703 | 704 | [[package]] 705 | name = "udev" 706 | version = "0.7.0" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "4ebdbbd670373442a12fe9ef7aeb53aec4147a5a27a00bbc3ab639f08f48191a" 709 | dependencies = [ 710 | "libc", 711 | "libudev-sys", 712 | "pkg-config", 713 | ] 714 | 715 | [[package]] 716 | name = "unicode-ident" 717 | version = "1.0.5" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 720 | 721 | [[package]] 722 | name = "uninitialized" 723 | version = "0.0.2" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "74c1aa4511c38276c548406f0b1f5f8b793f000cfb51e18f278a102abd057e81" 726 | 727 | [[package]] 728 | name = "unsafe-libyaml" 729 | version = "0.2.4" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68" 732 | 733 | [[package]] 734 | name = "value-bag" 735 | version = "1.0.0-alpha.9" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" 738 | dependencies = [ 739 | "ctor", 740 | "sval", 741 | "version_check", 742 | ] 743 | 744 | [[package]] 745 | name = "version_check" 746 | version = "0.9.4" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 749 | 750 | [[package]] 751 | name = "void" 752 | version = "1.0.2" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 755 | 756 | [[package]] 757 | name = "widestring" 758 | version = "1.0.2" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" 761 | 762 | [[package]] 763 | name = "winapi" 764 | version = "0.3.9" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 767 | dependencies = [ 768 | "winapi-i686-pc-windows-gnu", 769 | "winapi-x86_64-pc-windows-gnu", 770 | ] 771 | 772 | [[package]] 773 | name = "winapi-i686-pc-windows-gnu" 774 | version = "0.4.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 777 | 778 | [[package]] 779 | name = "winapi-util" 780 | version = "0.1.5" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 783 | dependencies = [ 784 | "winapi", 785 | ] 786 | 787 | [[package]] 788 | name = "winapi-x86_64-pc-windows-gnu" 789 | version = "0.4.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 792 | 793 | [[package]] 794 | name = "windows" 795 | version = "0.43.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" 798 | dependencies = [ 799 | "windows_aarch64_gnullvm", 800 | "windows_aarch64_msvc", 801 | "windows_i686_gnu", 802 | "windows_i686_msvc", 803 | "windows_x86_64_gnu", 804 | "windows_x86_64_gnullvm", 805 | "windows_x86_64_msvc", 806 | ] 807 | 808 | [[package]] 809 | name = "windows-sys" 810 | version = "0.42.0" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 813 | dependencies = [ 814 | "windows_aarch64_gnullvm", 815 | "windows_aarch64_msvc", 816 | "windows_i686_gnu", 817 | "windows_i686_msvc", 818 | "windows_x86_64_gnu", 819 | "windows_x86_64_gnullvm", 820 | "windows_x86_64_msvc", 821 | ] 822 | 823 | [[package]] 824 | name = "windows_aarch64_gnullvm" 825 | version = "0.42.0" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 828 | 829 | [[package]] 830 | name = "windows_aarch64_msvc" 831 | version = "0.42.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 834 | 835 | [[package]] 836 | name = "windows_i686_gnu" 837 | version = "0.42.0" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 840 | 841 | [[package]] 842 | name = "windows_i686_msvc" 843 | version = "0.42.0" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 846 | 847 | [[package]] 848 | name = "windows_x86_64_gnu" 849 | version = "0.42.0" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 852 | 853 | [[package]] 854 | name = "windows_x86_64_gnullvm" 855 | version = "0.42.0" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 858 | 859 | [[package]] 860 | name = "windows_x86_64_msvc" 861 | version = "0.42.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 864 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ddcset" 3 | version = "0.1.0" 4 | authors = ["arcnmx"] 5 | edition = "2021" 6 | 7 | description = "DDC/CI display control application" 8 | keywords = ["ddc", "vcp", "mccs", "ddccontrol", "ddcutil"] 9 | 10 | repository = "https://github.com/arcnmx/ddcset-rs" 11 | readme = "README.md" 12 | license = "MIT" 13 | 14 | include = [ 15 | "/src/**.rs", 16 | "/build.rs", 17 | "/README*", 18 | "/COPYING*", 19 | ] 20 | 21 | [badges] 22 | travis-ci = { repository = "arcnmx/ddcset-rs" } 23 | maintenance = { status = "passively-maintained" } 24 | 25 | [profile.dev] 26 | panic = "abort" 27 | [profile.release] 28 | panic = "abort" 29 | opt-level = 2 30 | lto = true 31 | 32 | [dependencies] 33 | ddc-hi = { version = "0.5", features = ["log-kv"], git = "https://github.com/arcnmx/ddc-hi-rs", branch = "v0.5.x" } 34 | mccs-db = { version = "0.2", git = "https://github.com/arcnmx/mccs-rs", branch = "v0.2.x" } 35 | clap = { version = "^4", features = ["derive"] } 36 | anyhow = "1" 37 | env_logger = "0.10" 38 | log = "0.4" 39 | hex = "0.4" 40 | once_cell = "1" 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddcset 2 | 3 | [![release-badge][]][cargo] [![license-badge][]][license] 4 | 5 | `ddcset` is an application for controlling connected monitors over DDC/CI. 6 | 7 | 8 | ## Platforms 9 | 10 | Currently supported platforms: 11 | 12 | - Linux 13 | - `i2c-dev`: requires a supported graphics driver, and `modprobe i2c-dev` 14 | - Modern and open source drivers should support this. Older proprietary 15 | drivers such as fglrx may not work. 16 | - [NVIDIA GPUs will require additional configuration to work](#nvidia-drivers-on-linux). 17 | - Windows 18 | - Windows Monitor Configuration API provides limited DDC/CI support on all GPUs. 19 | - NVIDIA NVAPI provides improved DDC/CI support for supported GPUs. 20 | 21 | ## Installation 22 | 23 | [Binaries are available on some platforms](https://github.com/arcnmx/ddcset-rs/releases). 24 | [Cargo](https://www.rust-lang.org/en-US/install.html) can also be used to install 25 | directly from source: 26 | 27 | cargo install --force ddcset 28 | 29 | ### Development Builds 30 | 31 | Every pushed commit produces an associated build available for 32 | [download from GitHub Actions](https://github.com/arcnmx/ddcset-rs/actions). 33 | 34 | ## NVIDIA drivers on Linux 35 | 36 | The NVIDIA Linux drivers have had broken DDC/CI support for years now. [There are 37 | workarounds](http://www.ddcutil.com/nvidia/) but it seems that it is not 38 | currently possible to use DDC/CI over DisplayPort. 39 | 40 | [release-badge]: https://img.shields.io/crates/v/ddcset.svg?style=flat-square 41 | [cargo]: https://crates.io/crates/ddcset 42 | [license-badge]: https://img.shields.io/badge/license-MIT-ff69b4.svg?style=flat-square 43 | [license]: https://github.com/arcnmx/ddcset-rs/blob/master/COPYING 44 | -------------------------------------------------------------------------------- /ci.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: with pkgs; with lib; let 2 | ddcset-rs = import ./. { inherit pkgs; }; 3 | inherit (ddcset-rs) checks packages; 4 | artifactRoot = ".ci/artifacts"; 5 | artifacts = "${artifactRoot}/bin/ddcset*"; 6 | ddcset-checked = (packages.ddcset.override { 7 | buildType = "debug"; 8 | }).overrideAttrs (_: { 9 | doCheck = true; 10 | }); 11 | in { 12 | config = { 13 | name = "ddcset"; 14 | ci.gh-actions.enable = true; 15 | cache.cachix.arc.enable = true; 16 | channels = { 17 | nixpkgs = { 18 | # see https://github.com/arcnmx/nixexprs-rust/issues/10 19 | args.config.checkMetaRecursively = false; 20 | version = "22.11"; 21 | }; 22 | }; 23 | tasks = { 24 | build.inputs = singleton ddcset-checked; 25 | fmt.inputs = singleton checks.rustfmt; 26 | }; 27 | jobs = { 28 | nixos = { 29 | tasks = { 30 | build-windows.inputs = singleton packages.ddcset-w64; 31 | build-static.inputs = singleton packages.ddcset-static; 32 | }; 33 | artifactPackages = { 34 | musl64 = packages.ddcset-static; 35 | win64 = packages.ddcset-w64; 36 | }; 37 | }; 38 | macos = { 39 | system = "x86_64-darwin"; 40 | artifactPackages.macos = ddcset-checked; 41 | }; 42 | }; 43 | 44 | # XXX: symlinks are not followed, see https://github.com/softprops/action-gh-release/issues/182 45 | artifactPackage = runCommand "ddcset-artifacts" { } ('' 46 | mkdir -p $out/bin 47 | '' + concatStringsSep "\n" (mapAttrsToList (key: ddcset: '' 48 | cp ${ddcset}/bin/ddcset${ddcset.stdenv.hostPlatform.extensions.executable} $out/bin/ddcset-${key}${ddcset.stdenv.hostPlatform.extensions.executable} 49 | '') config.artifactPackages)); 50 | 51 | gh-actions = { 52 | jobs = mkIf (config.id != "ci") { 53 | ${config.id} = { 54 | permissions = { 55 | contents = "write"; 56 | }; 57 | step = { 58 | artifact-build = { 59 | order = 1100; 60 | name = "artifact build"; 61 | uses = { 62 | # XXX: a very hacky way of getting the runner 63 | inherit (config.gh-actions.jobs.${config.id}.step.ci-setup.uses) owner repo version; 64 | path = "actions/nix/build"; 65 | }; 66 | "with" = { 67 | file = ""; 68 | attrs = "config.jobs.${config.jobId}.artifactPackage"; 69 | out-link = artifactRoot; 70 | }; 71 | }; 72 | artifact-upload = { 73 | order = 1110; 74 | name = "artifact upload"; 75 | uses.path = "actions/upload-artifact@v3"; 76 | "with" = { 77 | name = "ddcset"; 78 | path = artifacts; 79 | }; 80 | }; 81 | release-upload = { 82 | order = 1111; 83 | name = "release"; 84 | "if" = "startsWith(github.ref, 'refs/tags/')"; 85 | uses.path = "softprops/action-gh-release@v1"; 86 | "with".files = artifacts; 87 | }; 88 | }; 89 | }; 90 | }; 91 | }; 92 | }; 93 | options = { 94 | artifactPackage = mkOption { 95 | type = types.package; 96 | }; 97 | artifactPackages = mkOption { 98 | type = with types; attrsOf package; 99 | }; 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | lockData = builtins.fromJSON (builtins.readFile ./flake.lock); 3 | sourceInfo = lockData.nodes.std.locked; 4 | src = fetchTarball { 5 | url = "https://github.com/${sourceInfo.owner}/${sourceInfo.repo}/archive/${sourceInfo.rev}.tar.gz"; 6 | sha256 = sourceInfo.narHash; 7 | }; 8 | in (import src).Flake.Bootstrap { 9 | path = ./.; 10 | inherit lockData; 11 | loadWith.defaultPackage = "ddcset"; 12 | } 13 | -------------------------------------------------------------------------------- /derivation.nix: -------------------------------------------------------------------------------- 1 | let 2 | self = import ./. { pkgs = null; system = null; }; 3 | in { 4 | rustPlatform 5 | , nix-gitignore 6 | , udev 7 | , python3, pkg-config 8 | , hostPlatform 9 | , lib 10 | , libiconv, CoreGraphics ? darwin.apple_sdk.frameworks.CoreGraphics, darwin 11 | , buildType ? "release" 12 | , cargoLock ? crate.cargoLock 13 | , source ? crate.src 14 | , crate ? self.lib.crate 15 | }: with lib; rustPlatform.buildRustPackage { 16 | pname = crate.name; 17 | inherit (crate) version; 18 | 19 | buildInputs = 20 | optionals hostPlatform.isLinux [ udev ] 21 | ++ optionals hostPlatform.isDarwin [ libiconv CoreGraphics ]; 22 | nativeBuildInputs = [ pkg-config python3 ]; 23 | 24 | src = source; 25 | inherit cargoLock buildType; 26 | doCheck = false; 27 | 28 | meta = { 29 | description = "DDC/CI display control application"; 30 | homepage = "https://github.com/arcnmx/ddcset-rs"; 31 | license = licenses.mit; 32 | maintainers = [ maintainers.arcnmx ]; 33 | platforms = platforms.unix ++ platforms.windows; 34 | mainProgram = "ddcset"; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /eudev-gettid.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/shared/missing.h b/src/shared/missing.h 2 | index 1967840cd..521157708 100644 3 | --- a/src/shared/missing.h 4 | +++ b/src/shared/missing.h 5 | @@ -107,11 +107,10 @@ struct btrfs_ioctl_vol_args { 6 | #define MS_PRIVATE (1 << 18) 7 | #endif 8 | 9 | -#if !HAVE_DECL_GETTID 10 | +#define gettid gettid_syscall 11 | static inline pid_t gettid(void) { 12 | return (pid_t) syscall(SYS_gettid); 13 | } 14 | -#endif 15 | 16 | #ifndef MS_REC 17 | #define MS_REC 16384 18 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fl-config": { 4 | "locked": { 5 | "lastModified": 1653159448, 6 | "narHash": "sha256-PvB9ha0r4w6p412MBPP71kS/ZTBnOjxL0brlmyucPBA=", 7 | "owner": "flakelib", 8 | "repo": "fl", 9 | "rev": "fcefb9738d5995308a24cda018a083ccb6b0f460", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "flakelib", 14 | "ref": "config", 15 | "repo": "fl", 16 | "type": "github" 17 | } 18 | }, 19 | "flakelib": { 20 | "inputs": { 21 | "fl-config": "fl-config", 22 | "std": "std" 23 | }, 24 | "locked": { 25 | "lastModified": 1669759641, 26 | "narHash": "sha256-Zc6rZC0gTLZ9AzGG6vlImkYZGRf0bdMNk1fwS06ZqnU=", 27 | "owner": "flakelib", 28 | "repo": "fl", 29 | "rev": "ac03e98777f3f03e694f1bcf0c6a6a304734a263", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "flakelib", 34 | "repo": "fl", 35 | "type": "github" 36 | } 37 | }, 38 | "nix-std": { 39 | "locked": { 40 | "lastModified": 1652644856, 41 | "narHash": "sha256-tRRx4bBFwctWrUhnCNrcRee8Wiol1M7ypQTOPrLLtkc=", 42 | "owner": "flakelib", 43 | "repo": "std", 44 | "rev": "194230d45e99ce84cf25ea8b3ae4774601fc097c", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "flakelib", 49 | "ref": "master", 50 | "repo": "std", 51 | "type": "github" 52 | } 53 | }, 54 | "nixpkgs": { 55 | "locked": { 56 | "lastModified": 1670918062, 57 | "narHash": "sha256-iOhkyBYUU9Jfkk0lvI4ahpjyrTsLXj9uyJWwmjKg+gg=", 58 | "owner": "NixOS", 59 | "repo": "nixpkgs", 60 | "rev": "84575b0bd882be979516f4fecfe4d7c8de8f6a92", 61 | "type": "github" 62 | }, 63 | "original": { 64 | "id": "nixpkgs", 65 | "type": "indirect" 66 | } 67 | }, 68 | "root": { 69 | "inputs": { 70 | "flakelib": "flakelib", 71 | "nixpkgs": "nixpkgs", 72 | "rust": "rust" 73 | } 74 | }, 75 | "rust": { 76 | "inputs": { 77 | "nixpkgs": [ 78 | "nixpkgs" 79 | ] 80 | }, 81 | "locked": { 82 | "lastModified": 1670892108, 83 | "narHash": "sha256-CmfMxBxQNV4j56EgbCRIu++Npwl1xtwoYWoZPnhsSTo=", 84 | "owner": "arcnmx", 85 | "repo": "nixexprs-rust", 86 | "rev": "e7caa9e978907aa7d4a6c244eb69bff1fc987d10", 87 | "type": "github" 88 | }, 89 | "original": { 90 | "owner": "arcnmx", 91 | "repo": "nixexprs-rust", 92 | "type": "github" 93 | } 94 | }, 95 | "std": { 96 | "inputs": { 97 | "nix-std": "nix-std" 98 | }, 99 | "locked": { 100 | "lastModified": 1669759267, 101 | "narHash": "sha256-jBAtT8Hb9XwiW5cBDl2SmOMbRivGoi9H6t78fJUO53g=", 102 | "owner": "flakelib", 103 | "repo": "std", 104 | "rev": "628acb137d798d59d638999da4e1715572f18edb", 105 | "type": "github" 106 | }, 107 | "original": { 108 | "owner": "flakelib", 109 | "repo": "std", 110 | "type": "github" 111 | } 112 | } 113 | }, 114 | "root": "root", 115 | "version": 7 116 | } 117 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "DDC/CI display control application for Windows and Linux"; 3 | inputs = { 4 | flakelib.url = "github:flakelib/fl"; 5 | nixpkgs = { }; 6 | rust = { 7 | url = "github:arcnmx/nixexprs-rust"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | }; 11 | outputs = { self, flakelib, nixpkgs, rust, ... }@inputs: let 12 | nixlib = nixpkgs.lib; 13 | in flakelib { 14 | inherit inputs; 15 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 16 | devShells = { 17 | plain = { 18 | mkShell, writeShellScriptBin, hostPlatform 19 | , udev 20 | , pkg-config, python3 21 | , libiconv 22 | , CoreGraphics ? darwin.apple_sdk.frameworks.CoreGraphics, darwin 23 | , enableRust ? true, cargo 24 | , rustTools ? [ ] 25 | , nativeBuildInputs ? [ ] 26 | }: mkShell { 27 | inherit rustTools; 28 | buildInputs = 29 | nixlib.optional hostPlatform.isLinux udev 30 | ++ nixlib.optionals hostPlatform.isDarwin [ libiconv CoreGraphics ]; 31 | nativeBuildInputs = [ pkg-config python3 ] 32 | ++ nixlib.optional enableRust cargo 33 | ++ nativeBuildInputs ++ [ 34 | (writeShellScriptBin "generate" ''nix run .#generate "$@"'') 35 | ]; 36 | RUST_LOG = "ddc=debug"; 37 | }; 38 | stable = { rust'stable, outputs'devShells'plain }: outputs'devShells'plain.override { 39 | inherit (rust'stable) mkShell; 40 | enableRust = false; 41 | }; 42 | dev = { rust'unstable, rust-w64-overlay, rust-w64, outputs'devShells'plain }: let 43 | channel = rust'unstable.override { 44 | channelOverlays = [ rust-w64-overlay ]; 45 | }; 46 | in outputs'devShells'plain.override { 47 | inherit (channel) mkShell; 48 | enableRust = false; 49 | rustTools = [ "rust-analyzer" ]; 50 | nativeBuildInputs = [ rust-w64.pkgs.stdenv.cc.bintools ]; 51 | }; 52 | default = { outputs'devShells }: outputs'devShells.plain; 53 | }; 54 | packages = { 55 | ddcset = { 56 | __functor = _: import ./derivation.nix; 57 | fl'config.args = { 58 | crate.fallback = self.lib.crate; 59 | }; 60 | }; 61 | ddcset-w64 = { pkgsCross'mingwW64, rust-w64, source }: pkgsCross'mingwW64.callPackage ./derivation.nix { 62 | inherit (rust-w64.latest) rustPlatform; 63 | inherit source; 64 | }; 65 | ddcset-static = { pkgsCross'musl64'pkgsStatic, eudev-musl64, source }: (pkgsCross'musl64'pkgsStatic.callPackage ./derivation.nix { 66 | inherit ((import inputs.rust { pkgs = pkgsCross'musl64'pkgsStatic; }).latest) rustPlatform; 67 | udev = eudev-musl64; 68 | inherit source; 69 | }).overrideAttrs (old: { 70 | # XXX: why is this needed? 71 | NIX_LDFLAGS = old.NIX_LDFLAGS or "" + " -static"; 72 | RUSTFLAGS = old.RUSTFLAGS or "" + " -C default-linker-libraries=yes"; 73 | }); 74 | default = { ddcset }: ddcset; 75 | }; 76 | legacyPackages = { callPackageSet }: callPackageSet { 77 | source = { rust'builders }: rust'builders.wrapSource self.lib.crate.src; 78 | 79 | rust-w64 = { pkgsCross'mingwW64 }: import inputs.rust { inherit (pkgsCross'mingwW64) pkgs; }; 80 | rust-w64-overlay = { rust-w64 }: let 81 | target = rust-w64.lib.rustTargetEnvironment { 82 | inherit (rust-w64) pkgs; 83 | rustcFlags = [ "-L native=${rust-w64.pkgs.windows.pthreads}/lib" ]; 84 | }; 85 | in cself: csuper: { 86 | sysroot-std = csuper.sysroot-std ++ [ cself.manifest.targets.${target.triple}.rust-std ]; 87 | cargo-cc = csuper.cargo-cc // cself.context.rlib.cargoEnv { 88 | inherit target; 89 | }; 90 | rustc-cc = csuper.rustc-cc // cself.context.rlib.rustcCcEnv { 91 | inherit target; 92 | }; 93 | }; 94 | eudev-musl64 = { pkgsCross'musl64'pkgsStatic, gperf }: (pkgsCross'musl64'pkgsStatic.eudev.override { 95 | glib = null; gperf = null; util-linux = null; kmod = null; 96 | }).overrideAttrs (old: { 97 | # XXX: apply hack to fix https://github.com/NixOS/nixpkgs/pull/145819 98 | nativeBuildInputs = old.nativeBuildInputs ++ [ gperf ]; 99 | patches = old.patches or [ ] ++ [ ./eudev-gettid.patch ]; 100 | }); 101 | 102 | generate = { rust'builders, outputHashes }: rust'builders.generateFiles { 103 | paths = { 104 | "lock.nix" = outputHashes; 105 | }; 106 | }; 107 | outputHashes = { rust'builders }: rust'builders.cargoOutputHashes { 108 | inherit (self.lib) crate; 109 | }; 110 | } { }; 111 | checks = { 112 | rustfmt = { rust'builders, ddcset }: rust'builders.check-rustfmt-unstable { 113 | inherit (ddcset) src; 114 | config = ./.rustfmt.toml; 115 | }; 116 | }; 117 | lib = with nixlib; { 118 | crate = rust.lib.importCargo { 119 | path = ./Cargo.toml; 120 | inherit (import ./lock.nix) outputHashes; 121 | }; 122 | inherit (self.lib.crate) version; 123 | releaseTag = "v${self.lib.version}"; 124 | }; 125 | config = rec { 126 | name = "ddcset-rs"; 127 | }; 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /lock.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputHashes = { 3 | "ddc-0.3.0" = "sha256-n36HNhYbcu1FbvzcyArdwdsJj0yiXBuu9a/Zz91v30o="; 4 | "ddc-hi-0.5.0" = "sha256-LD5ZuG9pXaaVrD92TeHiytGVlZw3T6W0+3KzH9ww/QA="; 5 | "ddc-i2c-0.3.0" = "sha256-uzOzEfADIRmpnV1EKeWhDcoXFyv6jtbiBdl+y2LAMFA="; 6 | "ddc-macos-0.3.0" = "sha256-O8OQKke4fGvBbM3pvc/6/8d4IkebbMaVKkbaPFVe4I8="; 7 | "ddc-winapi-0.3.0" = "sha256-PHKXuaX7ZdqTIPd6jV/iItpQ3YYCjSIwMwaR1MdoJy4="; 8 | "i2c-linux-0.2.0" = "sha256-mBEZbbRW9csQ2t/m2n0AMQGlf67252y2+cT3OHE6P7c="; 9 | "mccs-0.2.0" = "sha256-HULGP1ciDbknV7KdBvm5Sa1SoMf3jKyt1lyUNusjITQ="; 10 | "mccs-caps-0.2.0" = "sha256-HULGP1ciDbknV7KdBvm5Sa1SoMf3jKyt1lyUNusjITQ="; 11 | "mccs-db-0.2.0" = "sha256-HULGP1ciDbknV7KdBvm5Sa1SoMf3jKyt1lyUNusjITQ="; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }@args: (import ./. args).devShells.default 2 | -------------------------------------------------------------------------------- /src/capabilities.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Error, 3 | clap::Args, 4 | ddc_hi::Display, 5 | ddcset::{Config, DisplayCommand}, 6 | mccs_db::ValueType, 7 | }; 8 | 9 | /// Query display capabilities 10 | #[derive(Args, Debug)] 11 | pub struct Capabilities {} 12 | 13 | impl DisplayCommand for Capabilities { 14 | const NAME: &'static str = "capabilities"; 15 | 16 | fn process(&mut self, _args: &Config, display: &mut Display) -> Result<(), Error> { 17 | println!("Display on {}:", display.backend()); 18 | println!("\tID: {}", display.id); 19 | display.update_fast(true)?; 20 | let mccs_database = display.mccs_database().unwrap_or_default(); 21 | for feature in (0..0x100).filter_map(|v| mccs_database.get(v as _)) { 22 | println!( 23 | "\tFeature 0x{:02x}: {}", 24 | feature.code, 25 | feature.name.as_ref().map(|v| &v[..]).unwrap_or("Unknown") 26 | ); 27 | println!("\t\tAccess: {:?}", feature.access); 28 | if feature.mandatory { 29 | println!("\t\tRequired"); 30 | } 31 | if let Some(group) = feature.group.as_ref() { 32 | println!("\t\tGroup: {}", group); 33 | } 34 | if !feature.interacts_with.is_empty() { 35 | println!("\t\tInteracts:"); 36 | for code in &feature.interacts_with { 37 | println!("\t\t\t{:02x}", code); 38 | } 39 | } 40 | match feature.ty { 41 | ValueType::Unknown => (), 42 | ValueType::Continuous { .. } => println!("\t\tType: Continuous"), 43 | ValueType::NonContinuous { .. } => println!("\t\tType: Non-Continuous"), 44 | ValueType::Table { .. } => println!("\t\tType: Table"), 45 | } 46 | if let Some(desc) = feature.description.as_ref() { 47 | println!("\t\t{}", desc); 48 | } 49 | match feature.ty { 50 | ValueType::NonContinuous { ref values, .. } => 51 | for (value, name) in values { 52 | println!( 53 | "\t\t\t0x{:02x}: {}", 54 | value, 55 | name.as_ref().map(|v| &v[..]).unwrap_or("Unknown") 56 | ); 57 | }, 58 | _ => (), 59 | } 60 | } 61 | 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/detect.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Error, 3 | clap::Args, 4 | ddc_hi::Display, 5 | ddcset::{Config, DisplayCommand}, 6 | log::{as_error, warn}, 7 | }; 8 | 9 | /// List detected displays 10 | #[derive(Args, Debug)] 11 | pub struct Detect {} 12 | 13 | impl DisplayCommand for Detect { 14 | const NAME: &'static str = "detect"; 15 | 16 | fn process(&mut self, args: &Config, display: &mut Display) -> Result<(), Error> { 17 | println!("Display on {}:", display.backend()); 18 | println!("\tID: {}", display.id); 19 | 20 | if let Err(e) = display.update_fast(false) { 21 | warn!( 22 | command = "detect", 23 | operation = "update_fast", 24 | error = as_error!(e), 25 | display = display; 26 | "failed to retrieve {display} info: {e}" 27 | ); 28 | } 29 | 30 | let info = display.info(); 31 | let res = if args.request_caps { 32 | display.update_all() 33 | } else { 34 | Ok(drop(display.update_version())) 35 | }; 36 | if let Err(e) = res { 37 | warn!( 38 | command = "detect", 39 | operation = "update_all", 40 | error = as_error!(e), 41 | display = display; 42 | "Failed to query {display}: {e}" 43 | ); 44 | } 45 | 46 | if let Some(value) = info.manufacturer_id.as_ref() { 47 | println!("\tManufacturer ID: {}", value); 48 | } 49 | if let Some(value) = info.model_name.as_ref() { 50 | println!("\tModel: {}", value); 51 | } 52 | if let Some(value) = info.serial_number.as_ref() { 53 | println!("\tSerial: {}", value); 54 | } 55 | if let Some(value) = info.mccs_version.as_ref() { 56 | println!("\tMCCS: {}", value); 57 | } else { 58 | println!("\tMCCS: Unavailable"); 59 | } 60 | 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/getvcp.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::{anyhow, Error}, 3 | clap::Args, 4 | ddc_hi::{traits::*, Display, FeatureCode}, 5 | ddcset::{Config, DisplayCommand}, 6 | log::{as_debug, error}, 7 | mccs_db::{Access, Descriptor, TableInterpretation, ValueInterpretation, ValueType}, 8 | }; 9 | 10 | /// Get VCP feature value 11 | #[derive(Args, Debug)] 12 | pub struct GetVCP { 13 | /// Feature code 14 | #[arg(value_parser(crate::util::parse_feature))] 15 | pub feature_code: Vec, 16 | /// Show raw value 17 | #[arg(short, long)] 18 | pub raw: bool, 19 | /// Read as table value 20 | #[arg(short, long)] 21 | pub table: bool, 22 | /// Scan all VCP feature codes 23 | #[arg(short, long)] 24 | pub scan: bool, 25 | } 26 | 27 | impl GetVCP { 28 | fn get_code(&mut self, display: &mut Display, code: FeatureCode, feature: Option<&Descriptor>) -> Result<(), Error> { 29 | let handle = &mut display.handle; 30 | if let Some(feature) = feature { 31 | if feature.access == Access::WriteOnly { 32 | println!("\tFeature 0x{:02x} is write-only", code); 33 | return Ok(()) 34 | } 35 | 36 | match feature.ty { 37 | ValueType::Unknown => { 38 | let value = handle.get_vcp_feature(code)?; 39 | println!( 40 | "\tFeature 0x{:02x} = {}", 41 | code, 42 | ValueInterpretation::Continuous.format(&value) 43 | ); 44 | }, 45 | ValueType::Continuous { mut interpretation } => { 46 | let value = handle.get_vcp_feature(code)?; 47 | if self.raw { 48 | interpretation = ValueInterpretation::Continuous; 49 | } 50 | println!("\tFeature 0x{:02x} = {}", feature.code, interpretation.format(&value)) 51 | }, 52 | ValueType::NonContinuous { 53 | ref values, 54 | mut interpretation, 55 | } => { 56 | if self.raw { 57 | interpretation = ValueInterpretation::Continuous; 58 | } 59 | 60 | let value = handle.get_vcp_feature(code)?; 61 | if let Some(&Some(ref name)) = values.get(&(value.value() as u8)) { 62 | println!( 63 | "\tFeature 0x{:02x} = {}: {}", 64 | feature.code, 65 | interpretation.format(&value), 66 | name 67 | ) 68 | } else { 69 | println!("\tFeature 0x{:02x} = {}", feature.code, interpretation.format(&value)) 70 | } 71 | }, 72 | ValueType::Table { mut interpretation } => { 73 | if self.raw { 74 | interpretation = TableInterpretation::Generic; 75 | } 76 | 77 | let value = handle.table_read(code)?; 78 | println!( 79 | "\tFeature 0x{:02x} = {}", 80 | code, 81 | interpretation 82 | .format(&value) 83 | .map_err(|_| anyhow!("table interpretation failed"))? 84 | ); 85 | }, 86 | } 87 | } else { 88 | if self.table { 89 | let value = handle.table_read(code)?; 90 | println!( 91 | "\tFeature 0x{:02x} = {}", 92 | code, 93 | TableInterpretation::Generic.format(&value).unwrap() 94 | ); 95 | } else { 96 | let value = handle.get_vcp_feature(code)?; 97 | println!( 98 | "\tFeature 0x{:02x} = {}", 99 | code, 100 | ValueInterpretation::Continuous.format(&value) 101 | ); 102 | }; 103 | } 104 | 105 | Ok(()) 106 | } 107 | } 108 | 109 | impl DisplayCommand for GetVCP { 110 | const NAME: &'static str = "get-vcp"; 111 | 112 | fn process(&mut self, args: &Config, display: &mut Display) -> Result<(), Error> { 113 | println!("Display on {}:", display.backend()); 114 | println!("\tID: {}", display.id); 115 | let mut mccs_database = display.mccs_database().unwrap_or_default(); 116 | let codes = if !self.feature_code.is_empty() { 117 | let _ = display.update_fast(args.request_caps); 118 | self.feature_code.clone() 119 | } else { 120 | if !self.scan { 121 | display.update_capabilities()?; 122 | (0..0x100) 123 | .map(|v| v as FeatureCode) 124 | .filter(|&c| mccs_database.get(c).is_some()) 125 | .collect() 126 | } else { 127 | (0..0x100).map(|v| v as FeatureCode).collect() 128 | } 129 | }; 130 | if let Some(db) = display.mccs_database() { 131 | mccs_database = db; 132 | } 133 | 134 | let mut errors = Vec::new(); 135 | for code in codes { 136 | let feature = mccs_database.get(code); 137 | if let Err(e) = self.get_code(display, code, feature) { 138 | error!( 139 | target: "ddcset::get-vcp", 140 | command = "get-vcp", 141 | operation = "get_vcp_feature", 142 | feature_code = code, 143 | feature = as_debug!(feature), 144 | error = as_debug!(e), 145 | display = display; 146 | "Failed to get feature 0x{code:02x} for {display}: {e}" 147 | ); 148 | errors.push(e); 149 | } 150 | } 151 | 152 | errors.into_iter().next().map(Err).unwrap_or(Ok(())) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::{anyhow, Error}, 3 | ddc_hi::{traits::*, Display, Query}, 4 | log::{as_debug, debug, error, warn}, 5 | std::{ 6 | iter, 7 | sync::{ 8 | atomic::{AtomicUsize, Ordering}, 9 | Arc, Mutex, 10 | }, 11 | }, 12 | }; 13 | 14 | #[derive(Debug, Default, Clone)] 15 | pub struct Config { 16 | pub query: Query, 17 | pub needs_caps: bool, 18 | pub needs_edid: bool, 19 | pub request_caps: bool, 20 | } 21 | 22 | pub trait CliCommand { 23 | fn run(&mut self, sleep: &mut DisplaySleep, args: &Config) -> Result; 24 | } 25 | 26 | pub trait DisplayCommand { 27 | const NAME: &'static str; 28 | 29 | fn process(&mut self, args: &Config, display: &mut Display) -> Result<(), Error>; 30 | } 31 | 32 | impl CliCommand for T { 33 | fn run(&mut self, sleep: &mut DisplaySleep, args: &Config) -> Result { 34 | let mut errors = Vec::new(); 35 | for display in query_displays(args) { 36 | let mut display = display?; 37 | 38 | match self.process(args, &mut display) { 39 | Ok(()) => (), 40 | Err(e) => { 41 | error!( 42 | target: &format!("ddcset::{}", Self::NAME), 43 | command = Self::NAME, 44 | operation = "process", 45 | error = as_debug!(e), 46 | display = display; 47 | "failed to process {display}: {e}" 48 | ); 49 | errors.push(e); 50 | }, 51 | } 52 | 53 | sleep.add(display); 54 | } 55 | 56 | match errors.into_iter().next() { 57 | Some(e) => Err(e), 58 | None => Ok(0), 59 | } 60 | } 61 | } 62 | 63 | #[derive(Default)] 64 | pub struct DisplaySleep(Vec); 65 | 66 | impl DisplaySleep { 67 | fn add(&mut self, display: Display) { 68 | self.0.push(display) 69 | } 70 | } 71 | 72 | impl Drop for DisplaySleep { 73 | fn drop(&mut self) { 74 | debug!("Waiting for display communication delays before exit"); 75 | for display in self.0.iter_mut() { 76 | display.handle.sleep() 77 | } 78 | } 79 | } 80 | 81 | pub fn query_displays<'a>(args: &'a Config) -> impl Iterator> + 'a { 82 | let Config { 83 | ref query, needs_caps, .. 84 | } = *args; 85 | let errors = Arc::new(Mutex::new(Vec::new())); 86 | let display_count = Arc::new(AtomicUsize::new(0)); 87 | 88 | Display::enumerate_all() 89 | .into_iter() 90 | .filter_map({ 91 | let errors = errors.clone(); 92 | move |d| match d { 93 | Ok(d) => Some(d), 94 | Err(e) => { 95 | warn!( 96 | target: &format!("ddc_hi::enumerate::{}", e.backend().name()), 97 | "Failed to enumerate: {}", e 98 | ); 99 | errors.lock().unwrap().push(e); 100 | None 101 | }, 102 | } 103 | }) 104 | .filter_map(move |mut d| { 105 | if !needs_caps && query.matches(&d.info()) { 106 | return Some(d) 107 | } 108 | 109 | if let Err(e) = d.update_fast(needs_caps) { 110 | warn!( 111 | target: &format!("ddc_hi::enumerate::{}", d.backend().name()), 112 | "Failed to query {}: {}", d.id, e 113 | ); 114 | } 115 | 116 | match query.matches(&d.info()) { 117 | true => Some(d), 118 | false => None, 119 | } 120 | }) 121 | .map({ 122 | let display_count = display_count.clone(); 123 | move |v| { 124 | display_count.fetch_add(1, Ordering::AcqRel); 125 | 126 | Ok(v) 127 | } 128 | }) 129 | .chain(iter::from_fn(move || match display_count.load(Ordering::Acquire) { 130 | 0 => Some(Err(match errors.lock().unwrap().drain(..).next() { 131 | Some(e) => e.into(), 132 | None => anyhow!("no matching displays found"), 133 | })), 134 | _ => None, 135 | })) 136 | } 137 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{capabilities::Capabilities, detect::Detect, getvcp::GetVCP, save::SaveCurrentSettings, setvcp::SetVCP}, 3 | anyhow::Error, 4 | clap::{Args, Parser, Subcommand}, 5 | ddc_hi::{Backend, Query}, 6 | ddcset::{CliCommand, Config, DisplaySleep}, 7 | std::{ 8 | io::{self, Write}, 9 | process::exit, 10 | }, 11 | }; 12 | 13 | mod capabilities; 14 | mod detect; 15 | mod getvcp; 16 | mod save; 17 | mod setvcp; 18 | mod util; 19 | 20 | /// DDC/CI monitor control 21 | #[derive(Parser, Debug)] 22 | #[command(author, version, about)] 23 | struct Cli { 24 | #[command(flatten)] 25 | args: GlobalArgs, 26 | #[command(flatten)] 27 | filter: Filter, 28 | #[command(subcommand)] 29 | command: Command, 30 | } 31 | 32 | #[derive(Args, Debug)] 33 | struct Filter { 34 | /// Backend driver whitelist 35 | #[arg(short, long, number_of_values(1), value_parser(util::backend_parser()))] 36 | pub backend: Vec, 37 | /// Filter by matching backend ID 38 | #[arg(short, long)] 39 | pub id: Option, 40 | /// Filter by matching manufacturer ID 41 | #[arg(short = 'g', long = "mfg")] 42 | pub manufacturer: Option, 43 | /// Filter by matching model 44 | #[arg(short = 'l', long = "model")] 45 | pub model_name: Option, 46 | /// Filter by matching serial number 47 | #[arg(short = 'n', long = "sn")] 48 | pub serial: Option, 49 | // TODO: filter by index? winapi makes things difficult, nothing is identifying... 50 | } 51 | 52 | impl Filter { 53 | fn query(&self) -> Query { 54 | let mut query = Query::Any; 55 | if !self.backend.is_empty() { 56 | let backends = self.backend.iter().copied().map(Query::Backend).collect(); 57 | query = Query::And(vec![query, Query::Or(backends)]) 58 | } 59 | if let Some(id) = &self.id { 60 | query = Query::And(vec![query, Query::Id(id.into())]) 61 | } 62 | if let Some(manufacturer) = &self.manufacturer { 63 | query = Query::And(vec![query, Query::ManufacturerId(manufacturer.into())]) 64 | } 65 | if let Some(model) = &self.model_name { 66 | query = Query::And(vec![query, Query::ModelName(model.into())]); 67 | } 68 | if let Some(serial) = &self.serial { 69 | query = Query::And(vec![query, Query::SerialNumber(serial.into())]) 70 | } 71 | 72 | query 73 | } 74 | 75 | fn needs_caps(&self) -> bool { 76 | self.model_name.is_some() 77 | } 78 | 79 | fn needs_edid(&self) -> bool { 80 | self.manufacturer.is_some() || self.serial.is_some() 81 | } 82 | } 83 | 84 | #[derive(Args, Debug)] 85 | pub struct GlobalArgs { 86 | /// Read display capabilities 87 | #[arg(short, long)] 88 | pub capabilities: bool, 89 | } 90 | 91 | #[derive(Subcommand, Debug)] 92 | enum Command { 93 | Detect(Detect), 94 | #[command(alias = "caps")] 95 | Capabilities(Capabilities), 96 | #[command(alias = "getvcp", alias = "get")] 97 | GetVCP(GetVCP), 98 | #[command(alias = "setvcp", alias = "set")] 99 | SetVCP(SetVCP), 100 | #[command(alias = "save")] 101 | SaveCurrentSettings(SaveCurrentSettings), 102 | } 103 | 104 | fn main() { 105 | match main_result() { 106 | Ok(code) => exit(code), 107 | Err(e) => { 108 | let _ = writeln!(io::stderr(), "{}", e); 109 | exit(1); 110 | }, 111 | } 112 | } 113 | 114 | fn log_init() { 115 | use { 116 | env_logger::{Builder, Env}, 117 | log::LevelFilter, 118 | }; 119 | 120 | Builder::new() 121 | .filter_level(LevelFilter::Warn) 122 | .parse_env(Env::default()) 123 | .init() 124 | } 125 | 126 | fn main_result() -> Result { 127 | log_init(); 128 | 129 | let Cli { args, command, filter } = Cli::parse(); 130 | 131 | let config = Config { 132 | query: filter.query(), 133 | needs_caps: filter.needs_caps(), 134 | needs_edid: filter.needs_edid(), 135 | request_caps: args.capabilities, 136 | }; 137 | 138 | let mut sleep = DisplaySleep::default(); 139 | 140 | match command { 141 | Command::Detect(mut cmd) => cmd.run(&mut sleep, &config), 142 | Command::Capabilities(mut cmd) => cmd.run(&mut sleep, &config), 143 | Command::GetVCP(mut cmd) => cmd.run(&mut sleep, &config), 144 | Command::SetVCP(mut cmd) => cmd.run(&mut sleep, &config), 145 | Command::SaveCurrentSettings(mut cmd) => cmd.run(&mut sleep, &config), 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/save.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Error, 3 | clap::Args, 4 | ddc_hi::{traits::*, Display}, 5 | ddcset::{Config, DisplayCommand}, 6 | }; 7 | 8 | /// Request monitor to save settings 9 | #[derive(Args, Debug)] 10 | pub struct SaveCurrentSettings {} 11 | 12 | impl DisplayCommand for SaveCurrentSettings { 13 | const NAME: &'static str = "save-current-settings"; 14 | 15 | fn process(&mut self, _args: &Config, display: &mut Display) -> Result<(), Error> { 16 | display.handle.save_current_settings()?; 17 | 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/setvcp.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::{anyhow, Error}, 3 | clap::Args, 4 | ddc_hi::{traits::*, Display, FeatureCode}, 5 | ddcset::{Config, DisplayCommand}, 6 | }; 7 | 8 | /// Set VCP feature value 9 | #[derive(Args, Debug)] 10 | pub struct SetVCP { 11 | /// Feature code 12 | #[arg(value_parser(crate::util::parse_feature))] 13 | pub feature_code: FeatureCode, 14 | /// Value to set 15 | #[arg(required_unless_present = "table")] 16 | pub value: Option, 17 | /// Read value after writing 18 | #[arg(short, long)] 19 | pub verify: bool, 20 | /// Write a table value 21 | #[arg( 22 | short, 23 | long = "table", 24 | conflicts_with = "value", 25 | value_parser(crate::util::parse_hex_string) 26 | )] 27 | pub table: Option, 28 | /// Table write offset 29 | #[arg(short, long)] 30 | pub offset: Option, 31 | } 32 | 33 | impl DisplayCommand for SetVCP { 34 | const NAME: &'static str = "set-vcp"; 35 | 36 | fn process(&mut self, _args: &Config, display: &mut Display) -> Result<(), Error> { 37 | let value = match (self.value, &self.table) { 38 | (Some(value), None) => Ok(value), 39 | (None, Some(table)) => Err(table), 40 | _ => unreachable!(), 41 | }; 42 | 43 | println!("Display on {}:", display.backend()); 44 | println!("\tID: {}", display.id); 45 | 46 | match value { 47 | Ok(value) => display.handle.set_vcp_feature(self.feature_code, value), 48 | Err(ref table) => display 49 | .handle 50 | .table_write(self.feature_code, self.offset.unwrap_or_default(), &table), 51 | }?; 52 | 53 | if self.verify { 54 | let matches = match value { 55 | Ok(value) => display.handle.get_vcp_feature(self.feature_code)?.value() == value, 56 | Err(table) => &display.handle.table_read(self.feature_code)? == table, 57 | }; 58 | 59 | if !matches { 60 | return Err(anyhow!("Verification failed")) 61 | } 62 | } 63 | 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anyhow::Error, 3 | clap::builder::TypedValueParser, 4 | ddc_hi::{Backend, FeatureCode}, 5 | once_cell::sync::Lazy, 6 | std::str::FromStr, 7 | }; 8 | 9 | pub type HexString = Vec; 10 | pub fn parse_hex_string(s: &str) -> Result { 11 | hex::decode(s) 12 | } 13 | 14 | pub fn parse_feature(s: &str) -> Result { 15 | if s.starts_with("0x") { 16 | FeatureCode::from_str_radix(&s[2..], 16).map_err(Into::into) 17 | } else { 18 | FeatureCode::from_str(s).map_err(Into::into) 19 | } 20 | } 21 | 22 | pub fn backend_parser() -> impl TypedValueParser { 23 | clap::builder::PossibleValuesParser::from(Backend::values().iter().map(|b| b.name())) 24 | .try_map(|s| Backend::from_str(&s)) 25 | } 26 | 27 | #[derive(Copy, Clone, Debug)] 28 | pub struct BackendValue(Backend); 29 | 30 | impl clap::ValueEnum for BackendValue { 31 | fn value_variants<'a>() -> &'a [Self] { 32 | static VALID_BACKENDS: Lazy> = 33 | Lazy::new(|| Backend::values().iter().cloned().map(BackendValue).collect()); 34 | &VALID_BACKENDS 35 | } 36 | 37 | fn to_possible_value(&self) -> Option { 38 | Some(self.0.name().into()) 39 | } 40 | } 41 | --------------------------------------------------------------------------------