├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── systemd │ └── reddish-shift.service ├── build.rs ├── clippy.toml ├── config.toml ├── deny.toml ├── docs ├── redshift-colorramp.md ├── redshift-contributing.md ├── redshift-design.md ├── redshift-news.md └── redshift-readme.md ├── rustfmt.toml ├── scripts └── generate │ ├── Cargo.toml │ ├── build.rs │ └── src │ └── lib.rs ├── src ├── calc_colorramp.rs ├── calc_solar.rs ├── cli.rs ├── config.rs ├── coproduct.rs ├── error.rs ├── gamma_drm.rs ├── gamma_dummy.rs ├── gamma_randr.rs ├── gamma_vidmode.rs ├── gamma_win32gdi.rs ├── lib.rs ├── location_manual.rs ├── main.rs ├── types.rs ├── types_display.rs ├── types_parse.rs └── utils.rs └── typos.toml /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUSTFLAGS: -D warnings 12 | # RUSTDOCFLAGS: -D warnings 13 | 14 | jobs: 15 | check: 16 | name: Check 17 | timeout-minutes: 2 18 | permissions: 19 | # required for ppremk/lfs-warning 20 | pull-requests: read 21 | # required for LouisBrunner/checks-action 22 | checks: write 23 | contents: read 24 | 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: Swatinem/rust-cache@v2 29 | with: 30 | cache-all-crates: true 31 | cache-on-failure: true 32 | - uses: dtolnay/rust-toolchain@stable 33 | - uses: taiki-e/install-action@v2 34 | with: 35 | tool: typos,cargo-deny 36 | 37 | - name: Check spelling 38 | if: success() || failure() 39 | id: typos 40 | run: typos 41 | 42 | - name: Check dependencies 43 | if: success() || failure() 44 | id: cargo-deny 45 | run: cargo deny check 46 | 47 | - name: Check formatting 48 | if: success() || failure() 49 | id: cargo-fmt 50 | run: cargo fmt --all -- --check 51 | 52 | - name: Check file size 53 | if: (success() || failure()) && github.event_name == 'pull_request' 54 | id: lfs 55 | uses: mahor1221/lfs-warning@master 56 | with: 57 | filesizelimit: 256000 58 | sendComment: false 59 | 60 | # create check runs for pull requests 61 | 62 | - uses: LouisBrunner/checks-action@v2.0.0 63 | if: (success() || failure()) && github.event_name == 'pull_request' 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | name: Check file size 67 | conclusion: ${{ steps.lfs.outcome }} 68 | 69 | - uses: LouisBrunner/checks-action@v2.0.0 70 | if: (success() || failure()) && github.event_name == 'pull_request' 71 | with: 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | name: Check spelling 74 | conclusion: ${{ steps.typos.outcome }} 75 | 76 | - uses: LouisBrunner/checks-action@v2.0.0 77 | if: (success() || failure()) && github.event_name == 'pull_request' 78 | with: 79 | token: ${{ secrets.GITHUB_TOKEN }} 80 | name: Check dependencies 81 | conclusion: ${{ steps.cargo-deny.outcome }} 82 | 83 | - uses: LouisBrunner/checks-action@v2.0.0 84 | if: (success() || failure()) && github.event_name == 'pull_request' 85 | with: 86 | token: ${{ secrets.GITHUB_TOKEN }} 87 | name: Check formatting 88 | conclusion: ${{ steps.cargo-fmt.outcome }} 89 | 90 | check-target: 91 | name: Check target 92 | timeout-minutes: 5 93 | strategy: 94 | fail-fast: false 95 | matrix: 96 | include: 97 | - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, cmd: cross } 98 | - { os: ubuntu-latest, target: aarch64-unknown-linux-musl, cmd: cross } 99 | - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, cmd: cargo } 100 | - { os: ubuntu-latest, target: x86_64-unknown-linux-musl, cmd: cargo } 101 | # - { os: windows-latest, target: aarch64-pc-windows-msvc, cmd: cross } 102 | # - { os: windows-latest, target: x86_64-pc-windows-msvc, cmd: cargo } 103 | 104 | runs-on: ${{ matrix.os }} 105 | steps: 106 | - uses: actions/checkout@v4 107 | - uses: Swatinem/rust-cache@v2 108 | with: 109 | cache-all-crates: true 110 | cache-on-failure: true 111 | - uses: dtolnay/rust-toolchain@stable 112 | with: 113 | targets: ${{ matrix.target }} 114 | - if: matrix.cmd == 'cross' 115 | uses: taiki-e/install-action@v2 116 | with: 117 | tool: cross 118 | 119 | - name: Check build 120 | if: success() || failure() 121 | id: cargo-check 122 | run: ${{ matrix.cmd }} check --profile=ci --locked --target ${{ matrix.target }} 123 | 124 | - name: Check lints 125 | if: success() || failure() 126 | id: cargo-clippy 127 | run: ${{ matrix.cmd }} clippy --profile=ci --locked --target ${{ matrix.target }} 128 | 129 | - name: Check tests 130 | if: success() 131 | id: cargo-test 132 | run: ${{ matrix.cmd }} test --profile=ci --locked --target ${{ matrix.target }} 133 | 134 | - name: Check documents 135 | if: success() 136 | id: cargo-doc 137 | run: ${{ matrix.cmd }} doc --profile=ci --locked --target ${{ matrix.target }} --no-deps 138 | 139 | # create check runs for pull requests 140 | 141 | - uses: LouisBrunner/checks-action@v2.0.0 142 | if: (success() || failure()) && github.event_name == 'pull_request' 143 | with: 144 | token: ${{ secrets.GITHUB_TOKEN }} 145 | name: Check build ${{ matrix.target }} 146 | conclusion: ${{ steps.cargo-check.outcome }} 147 | 148 | - uses: LouisBrunner/checks-action@v2.0.0 149 | if: (success() || failure()) && github.event_name == 'pull_request' 150 | with: 151 | token: ${{ secrets.GITHUB_TOKEN }} 152 | name: Check lints ${{ matrix.target }} 153 | conclusion: ${{ steps.cargo-clippy.outcome }} 154 | 155 | - uses: LouisBrunner/checks-action@v2.0.0 156 | if: (success() || failure()) && github.event_name == 'pull_request' 157 | with: 158 | token: ${{ secrets.GITHUB_TOKEN }} 159 | name: Check tests ${{ matrix.target }} 160 | conclusion: ${{ steps.cargo-test.outcome }} 161 | 162 | - uses: LouisBrunner/checks-action@v2.0.0 163 | if: (success() || failure()) && github.event_name == 'pull_request' 164 | with: 165 | token: ${{ secrets.GITHUB_TOKEN }} 166 | name: Check documents ${{ matrix.target }} 167 | conclusion: ${{ steps.cargo-doc.outcome }} 168 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | run-name: "release: ${{ github.ref_name }}" 3 | 4 | on: 5 | create: 6 | tags: 7 | - 'v[0-9]+.[0-9]+.[0-9]+' 8 | 9 | env: 10 | PKG_NAME: reddish-shift 11 | CARGO_TERM_COLOR: always 12 | RUSTFLAGS: -D warnings 13 | # RUSTDOCFLAGS: -D warnings 14 | 15 | jobs: 16 | release: 17 | name: Release 18 | if: github.event_name == 'create' 19 | timeout-minutes: 5 20 | permissions: 21 | # required for taiki-e/create-gh-release-action 22 | contents: write 23 | 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: dtolnay/rust-toolchain@stable 28 | - uses: taiki-e/install-action@v2 29 | with: 30 | tool: cargo-tarpaulin 31 | 32 | - name: Generate assets for release 33 | run: cargo build --profile=ci --locked -p generate 34 | - name: Upload the assets 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: assets 38 | if-no-files-found: error 39 | path: | 40 | target/ci/completion 41 | target/ci/man1 42 | 43 | - name: Generate code coverage reports 44 | if: github.event_name == 'create' 45 | run: | 46 | cargo tarpaulin --profile=ci --locked \ 47 | --all-features --workspace --timeout 120 --out xml 48 | - name: Upload the reports to Codecov 49 | if: github.event_name == 'create' 50 | uses: codecov/codecov-action@v4 51 | with: 52 | fail_ci_if_error: true 53 | token: ${{ secrets.CODECOV_TOKEN }} 54 | 55 | - name: Create release 56 | uses: taiki-e/create-gh-release-action@v1 57 | with: 58 | draft: true 59 | branch: main 60 | changelog: CHANGELOG.md 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | release-target: 64 | name: Release target 65 | needs: release 66 | timeout-minutes: 10 67 | permissions: 68 | # required for taiki-e/upload-rust-binary-action 69 | contents: write 70 | strategy: 71 | fail-fast: false 72 | matrix: 73 | include: 74 | - { os: ubuntu-latest, target: aarch64-unknown-linux-gnu, cmd: cross } 75 | - { os: ubuntu-latest, target: aarch64-unknown-linux-musl, cmd: cross } 76 | - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, cmd: cargo } 77 | - { os: ubuntu-latest, target: x86_64-unknown-linux-musl, cmd: cargo } 78 | - { os: windows-latest, target: aarch64-pc-windows-msvc, cmd: cross } 79 | - { os: windows-latest, target: x86_64-pc-windows-msvc, cmd: cargo } 80 | # - { os: macos-latest, target: aarch64-apple-darwin } 81 | # - { os: macos-latest, target: x86_64-apple-darwin } 82 | # - { os: macos-latest, target: universal-apple-darwin } 83 | 84 | runs-on: ${{ matrix.os }} 85 | steps: 86 | - uses: actions/checkout@v4 87 | - uses: dtolnay/rust-toolchain@stable 88 | with: 89 | targets: ${{ matrix.target }} 90 | - if: matrix.cmd == 'cross' 91 | uses: taiki-e/install-action@v2 92 | with: 93 | tool: cross 94 | - if: matrix.target == 'x86_64-unknown-linux-gnu' 95 | uses: taiki-e/install-action@v2 96 | with: 97 | tool: cargo-deb 98 | 99 | - name: Install prerequisites 100 | shell: bash 101 | run: | 102 | case ${{ matrix.target }} in 103 | aarch64-unknown-linux-gnu) 104 | sudo apt-get -y update 105 | sudo apt-get -y install gcc-aarch64-linux-gnu;; 106 | esac 107 | 108 | - name: Download the assets 109 | uses: actions/download-artifact@v4 110 | with: 111 | name: assets 112 | - if: matrix.os != 'windows-latest' 113 | run: rm completion/_${{ env.PKG_NAME }}.ps1 114 | - name: Set assets 115 | id: assets 116 | shell: bash 117 | env: 118 | WINDOWS: completion/_${{ env.PKG_NAME }}.ps1 119 | MACOS: completion,man1 120 | LINUX: completion,man1,assets/systemd/${{ env.PKG_NAME }}.service 121 | run: | 122 | case ${{ matrix.os }} in 123 | ubuntu-latest) echo "assets=$LINUX" >> $GITHUB_OUTPUT;; 124 | windows-latest) echo "assets=$WINDOWS" >> $GITHUB_OUTPUT;; 125 | macos-latest) echo "assets=$MACOS" >> $GITHUB_OUTPUT;; 126 | esac 127 | 128 | - name: Build debian package 129 | if: matrix.target == 'x86_64-unknown-linux-gnu' 130 | id: deb 131 | run: | 132 | ${{ matrix.cmd }} build --release --target ${{ matrix.target }} --all 133 | cargo deb --no-build --target ${{ matrix.target }} 134 | dir="target/${{ matrix.target }}/debian" 135 | arch="$(a=${{ matrix.target }} && echo "${a%%-*}")" 136 | name="$PKG_NAME-$GITHUB_REF_NAME-$arch.deb" 137 | mv "$dir"/*.deb "$dir/$name" 138 | echo path="$dir/$name" >> $GITHUB_OUTPUT 139 | 140 | - name: Build and upload 141 | uses: taiki-e/upload-rust-binary-action@v1 142 | with: 143 | target: ${{ matrix.target }} 144 | bin: ${{ env.PKG_NAME }} 145 | archive: $bin-$tag-$target 146 | include: LICENSE,README.md,CHANGELOG.md,config.toml,${{ steps.assets.outputs.assets }} 147 | asset: ${{ steps.deb.outputs.path }} 148 | checksum: sha256 149 | token: ${{ secrets.GITHUB_TOKEN }} 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | +/ 2 | target/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Changelog 6 | All notable changes to this project will be documented in this file. 7 | 8 | This project adheres to [Semantic Versioning](https://semver.org). 9 | 10 | ## [Unreleased] 11 | 12 | ## [0.1.2] - 2024-07-17 13 | * Fix: Allow negative values in --location and --scheme arguments ([#1](https://github.com/mahor1221/reddish-shift/issues/1)) 14 | * Fix: Apply gamma ramps without checking if it's changed in daemon mode 15 | to restore the desired ramps faster when an external program changes them. See 16 | 'Why does the redness effect occasionally switch off for a few seconds?' in 17 | docs/redshift-readme.md 18 | * Fix: Minor changes to the set command 19 | * Remove the hard requirement of providing at least one of the temperature, 20 | gamma or brightness cli arguments 21 | * Always use the default values of temperature, gamma and brightness when they 22 | don't exist. Don't use the values provided by the config file 23 | * Feat: Add systemd service 24 | 25 | ## [0.1.1] - 2024-06-27 26 | * Fix AUR and Crates.io builds 27 | 28 | ## [0.1.0] - 2024-06-27 29 | * Initial release 30 | 31 | [Unreleased]: https://github.com/mahor1221/reddish-shift/compare/v0.1.2...HEAD 32 | [0.1.2]: https://github.com/mahor1221/reddish-shift/releases/tag/v0.1.2 33 | [0.1.1]: https://github.com/mahor1221/reddish-shift/releases/tag/v0.1.1 34 | [0.1.0]: https://github.com/mahor1221/reddish-shift/releases/tag/v0.1.0 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reddish-shift" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["Mahor Foruzesh "] 6 | license = "GPL-3.0-or-later" 7 | description = "Set color temperature of display according to time of day" 8 | readme = "README.md" 9 | repository = "https://github.com/mahor1221/reddish-shift" 10 | homepage = "https://github.com/mahor1221/reddish-shift" 11 | documentation = "https://github.com/mahor1221/reddish-shift" 12 | keywords = ["eye-strain", "gamma", "brightness", "sunrise", "sunset"] 13 | categories = ["command-line-utilities"] 14 | build = "build.rs" 15 | 16 | [workspace] 17 | members = ["scripts/generate"] 18 | 19 | [dependencies] 20 | thiserror = "1.0.61" 21 | const_format = "0.2.32" 22 | itertools = "0.13.0" 23 | frunk = "0.4.2" 24 | frunk_core = "0.4.2" 25 | 26 | # cli 27 | clap = { version = "4.5.7", features = ["derive"] } 28 | tracing = "0.1.40" 29 | tracing-subscriber = "0.3.18" 30 | anstream = "0.6.14" 31 | anstyle = "1.0.7" 32 | exitcode = "1.1.2" 33 | ctrlc = { version = "3.4.4", features = ["termination"] } 34 | 35 | # config 36 | dirs = "5.0.1" 37 | serde = { version = "1.0.203", features = ["derive"] } 38 | toml = "0.8.14" 39 | 40 | chrono = "0.4.38" 41 | 42 | [target.'cfg(unix)'.dependencies] 43 | x11rb = { version = "0.13.1", features = ["xf86vidmode", "randr"] } 44 | drm = "0.12.0" 45 | 46 | [target.'cfg(windows)'.dependencies] 47 | windows = { version = "0.57.0", features = [ 48 | "Win32_Graphics_Gdi", 49 | "Win32_UI_ColorSystem", 50 | ] } 51 | 52 | [dev-dependencies] 53 | insta = "1.39.0" 54 | anyhow = "1.0.86" 55 | 56 | [build-dependencies] 57 | anyhow = "1.0.86" 58 | cfg_aliases = "0.2.1" 59 | vergen = { version = "8.3.1", features = ["cargo", "git", "gitcl", "rustc"] } 60 | 61 | [workspace.lints.rust] 62 | unsafe_code = "warn" 63 | [workspace.lints.rustdoc] 64 | private-doc-tests = "warn" 65 | unescaped-backticks = "warn" 66 | [workspace.lints.clippy] 67 | panic = "deny" 68 | dbg-macro = "warn" 69 | decimal-literal-representation = "warn" 70 | expect-used = "deny" 71 | explicit-auto-deref = "warn" 72 | get-unwrap = "warn" 73 | manual-let-else = "warn" 74 | missing-enforced-import-renames = "warn" 75 | obfuscated-if-else = "warn" 76 | semicolon-outside-block = "warn" 77 | todo = "warn" 78 | too-many-lines = "warn" 79 | undocumented-unsafe-blocks = "deny" 80 | unimplemented = "warn" 81 | uninlined-format-args = "warn" 82 | unnested-or-patterns = "warn" 83 | unwrap-in-result = "warn" 84 | unwrap-used = "warn" 85 | use-debug = "warn" 86 | 87 | [lints] 88 | workspace = true 89 | 90 | [profile.release] 91 | lto = true 92 | strip = true 93 | codegen-units = 1 94 | [profile.ci] 95 | inherits = "dev" 96 | debug = false 97 | [profile.perf] 98 | inherits = "release" 99 | strip = false 100 | debug = 1 101 | 102 | [patch.crates-io] 103 | # See https://github.com/clap-rs/clap/issues/5550 104 | clap = { git = "https://github.com/mahor1221/clap.git", branch = "patch" } 105 | 106 | [package.metadata.deb] 107 | license-file = ["LICENSE", "0"] 108 | depends = "$auto" 109 | section = "utilities" 110 | assets = [ 111 | [ 112 | "target/release/reddish-shift", 113 | "usr/bin/", 114 | "755", 115 | ], 116 | [ 117 | "LICENSE", 118 | "usr/share/licenses/reddish-shift/", 119 | "644", 120 | ], 121 | [ 122 | "README.md", 123 | "usr/share/doc/reddish-shift/", 124 | "644", 125 | ], 126 | [ 127 | "CHANGELOG.md", 128 | "usr/share/doc/reddish-shift/", 129 | "644", 130 | ], 131 | [ 132 | "config.toml", 133 | "usr/share/doc/reddish-shift/", 134 | "644", 135 | ], 136 | [ 137 | "target/release/completion/_reddish-shift", 138 | "usr/share/zsh/site-functions/", 139 | "644", 140 | ], 141 | [ 142 | "target/release/completion/reddish-shift.bash", 143 | "usr/share/bash-completion/completions/reddish-shift", 144 | "644", 145 | ], 146 | [ 147 | "target/release/completion/reddish-shift.fish", 148 | "usr/share/fish/completions/", 149 | "644", 150 | ], 151 | [ 152 | "target/release/completion/reddish-shift.elv", 153 | "usr/share/elvish/lib/", 154 | "644", 155 | ], 156 | [ 157 | "target/release/man1/*", 158 | "usr/share/man/man1/", 159 | "644", 160 | ], 161 | [ 162 | "assets/systemd/reddish-shift.service", 163 | "/usr/lib/systemd/user/", 164 | "644", 165 | ], 166 | ] 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reddish Shift 2 | [![Build](https://img.shields.io/github/actions/workflow/status/mahor1221/reddish-shift/ci.yaml?logo=github)](https://github.com/mahor1221/reddish-shift/actions) 3 | [![Coverage](https://img.shields.io/codecov/c/github/mahor1221/reddish-shift?logo=codecov)](https://codecov.io/gh/mahor1221/reddish-shift) 4 | [![Crates.io](https://img.shields.io/crates/v/reddish-shift.svg?logo=rust)](https://crates.io/crates/reddish-shift) 5 | [![Support](https://img.shields.io/badge/support-7289da.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/E6uKg67f) 6 | 7 | A port of [Redshift](https://github.com/jonls/redshift). 8 | Translated line by line with the help of [C2Rust](https://github.com/immunant/c2rust). 9 | 10 | Reddish Shift adjusts the color temperature of your screen according to your 11 | surroundings. This may help your eyes hurt less if you are working in front of 12 | the screen at night. 13 | 14 | 15 | 16 | ## Installation 17 | [![REPOSITORIES](https://repology.org/badge/vertical-allrepos/reddish-shift.svg?columns=3&exclude_unsupported=1)](https://repology.org/project/reddish-shift) 18 | 19 |
20 | Cargo 21 | 22 | ```bash 23 | cargo install reddish-shift 24 | ``` 25 |
26 | 27 |
28 | Archlinux 29 | 30 | ```bash 31 | paru -S reddish-shift 32 | paru -S reddish-shift-bin 33 | paru -S reddish-shift-git 34 | ``` 35 |
36 | 37 | 38 | 39 | ## Usage 40 | For a quick start, run: 41 | ```bash 42 | reddish-shift daemon --location LATITUDE:LONGITUDE 43 | ``` 44 | replace `LATITUDE` and `LONGITUDE` with your current geolocation. 45 | 46 | To see all available commands: 47 | ```bash 48 | reddish-shift -h 49 | ``` 50 | 51 | To see all available options for a given command (e.g. daemon): 52 | ```bash 53 | reddish-shift daemon --help 54 | ``` 55 | Note that using `--help` instead of `-h` prints a more detailed help message. 56 | 57 | A [configuration file](config.toml) can also be used. It should be saved in 58 | the following location depending on the platform: 59 | * Linux: `$XDG_CONFIG_HOME/reddish-shift/config.toml` 60 | or `$HOME/.config/reddish-shift/config.toml` if `$XDG_CONFIG_HOME` is not set 61 | or `/etc/reddish-shift/config.toml` for system wide configuration 62 | * macOS: `$HOME/Library/Application Support/reddish-shift/config.toml` 63 | * Windows: `%AppData%\reddish-shift\config.toml` 64 | 65 | 66 | 67 | ## Building 68 | Run `cargo build --release --all` to build these files: 69 | - `target/release/reddish-shift`: the main program 70 | - `target/release/man1/`: man pages 71 | - `target/release/completion/`: various shell completion scrips 72 | 73 | 74 | 75 | ## RoadMap 76 | * Linux 77 | * [x] XRANDR gamma adjustment 78 | * [x] XVidMode gamma adjustment 79 | * [ ] DRM gamma adjustment 80 | * [ ] reddish-shift-gtk (from redshift-gtk) 81 | * [ ] systemd service, apparmor config (from [redshift/data](https://github.com/jonls/redshift/tree/master/data)) 82 | * Windows 83 | * [ ] Win32gdi gamma adjustment 84 | * [ ] Support installation with: Appimage, AUR, DEB, PPA, MSI, Choco 85 | * [ ] Geoclue2 location provider 86 | * [ ] Real screen brightness control (experimental) 87 | * Supporting macOS is not planned currently. Contributions are welcomed. 88 | * [ ] Unit testing 89 | * [ ] Automatic Conversion from Redshift's config file to `reddish-shift/config.toml` 90 | 91 | 92 | 93 | ## License 94 | This project is licensed under the terms of [GNU General Public License v3.0](LICENSE). 95 | -------------------------------------------------------------------------------- /assets/systemd/reddish-shift.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Reddish Shift display colour temperature adjustment 3 | Documentation=man:reddish-shift(1) 4 | After=display-manager.service 5 | 6 | [Service] 7 | ExecStart=/usr/bin/reddish-shift daemon 8 | Restart=always 9 | 10 | [Install] 11 | WantedBy=default.target 12 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cfg_aliases::cfg_aliases; 3 | use vergen::EmitBuilder; 4 | 5 | fn main() -> Result<()> { 6 | cfg_aliases! { 7 | linux : { target_os = "linux" }, 8 | freebsd: { target_os = "freebsd" }, 9 | openbsd: { target_os = "openbsd" }, 10 | netbsd: { target_os = "netbsd" }, 11 | dragonfly: { target_os = "dragonfly" }, 12 | unix_without_macos: { any(linux, freebsd, openbsd, netbsd, dragonfly) }, 13 | } 14 | 15 | EmitBuilder::builder() 16 | .rustc_semver() 17 | .rustc_host_triple() 18 | .cargo_features() 19 | .cargo_target_triple() 20 | .fail_on_error() 21 | .emit()?; 22 | 23 | EmitBuilder::builder() 24 | .git_describe(false, false, None) 25 | .git_commit_date() 26 | .fail_on_error() 27 | .emit() 28 | .unwrap_or_else(|_| { 29 | println!("cargo::rustc-env=VERGEN_GIT_DESCRIBE="); 30 | println!("cargo::rustc-env=VERGEN_GIT_COMMIT_DATE="); 31 | }); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | too-many-lines-threshold = 40 2 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # Color temperature to set for day and night [default: 6500-4500] 2 | 3 | # The neutral temperature is 6500K. Using this value will not change the color 4 | # temperature of the display. Setting the color temperature to a value higher 5 | # than this results in more blue light, and setting a lower value will result 6 | # in more red light. 7 | #temperature = 5000 #(day=night=5000) 8 | #temperature = "6500-4500" #(day=6500, night=4500) 9 | temperature = "4600-3600" 10 | 11 | # Additional gamma correction to apply for day and night [default: 1.0] 12 | # 13 | # Either set it for all colors, or each color channel individually 14 | #gamma = 0.9 #(day=night=0.9) 15 | #gamma = "1.0 - 0.8:0.9:0.9" #(day=1.0, night=(R=0.8, G=0.9, B=0.9)) 16 | gamma = "0.9:0.9:0.9 - 0.85:0.9:0.9" 17 | 18 | # Screen brightness to apply for day and night [default: 1.0] 19 | # 20 | # It is a fake brightness adjustment obtained by manipulating the gamma ramps 21 | # which means that it does not reduce the backlight of the screen 22 | #brightness = 1.0 #(day=night=0.8) 23 | #brightness = "1.0-0.8" #(day=1.0, night=0.8) 24 | 25 | 26 | # Transition scheme [default: 3:-6] 27 | # 28 | # Either time ranges or elevation angles. By default, Reddish Shift will use 29 | # the current elevation of the sun to determine whether it is daytime, night 30 | # or in transition (dawn/dusk). You can also use the print command to see solar 31 | # elevation angles for the next 24 hours 32 | #scheme = "6:00-7:45 - 18:35-20:15" #(dawn=6:00-7:45, dusk=18:35-20:15) 33 | #scheme = "7:45 - 18:35" #(day starts at 7:45, night starts at 20:15) 34 | #scheme = "3:-6" #(above 3° is day, bellow -6° is night) 35 | 36 | 37 | # Location, used for computation of current solar elevation [default: 0:0] 38 | # 39 | # It is not needed when using manual time ranges for transition scheme Either 40 | # set latitude and longitude manually or select a location provider. Negative 41 | # values represent west and south, respectively. 42 | #location = "51.48:0.0" #(Greenwich) 43 | #location = "geoclue2" #(Currently not available) 44 | 45 | 46 | # Adjustment method to use to apply color settings 47 | # 48 | # If not set, the first available method will be used 49 | #method = "dummy" #(does not affect the display) 50 | # XVidMode extension 51 | #method = "vidmode" #(apply to $DISPLAY) 52 | #method = "vidmode:0" #(apply to screen 0) 53 | # XRANDR extension 54 | #method = "randr" #(apply to $DISPLAY) 55 | #method = "randr:0" #(apply to screen 0) 56 | #method = "randr$DISPLAY:62,63" #(apply to $DISPLAY with crtcs 62 and 63) 57 | # Direct Rendering Manager 58 | #method = "drm" #(apply to /dev/dri/card0) 59 | #method = "drm:1" #(apply to /dev/dri/card1) 60 | #method = "drm:0:80" #(apply to /dev/dri/card0 with crtc 80) 61 | # Windows graphics device interface: 62 | #method = "win32gdi" #(apply to current display) 63 | 64 | 65 | # Reset existing gamma ramps before applying new color settings 66 | #reset-ramps = false 67 | 68 | 69 | # Disable fading between color temperatures 70 | # 71 | # It will cause an immediate change between screen temperatures. by default, 72 | # the new screen temperature are gradually applied over a 73 | # couple of seconds 74 | #disable-fade = false 75 | 76 | 77 | # Duration of sleep between screen updates in milliseconds 78 | #sleep-duration = 5000 79 | 80 | 81 | # Duration of sleep between screen updates for fade in milliseconds 82 | #sleep-duration_short = 100 83 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # https://embarkstudios.github.io/cargo-deny 2 | [graph] 3 | all-features = true 4 | targets = [ 5 | "x86_64-unknown-linux-gnu", 6 | "aarch64-unknown-linux-gnu", 7 | "x86_64-unknown-linux-musl", 8 | "aarch64-apple-darwin", 9 | "x86_64-apple-darwin", 10 | "x86_64-pc-windows-msvc", 11 | ] 12 | 13 | [advisories] 14 | db-path = "$CARGO_HOME/advisory-dbs" 15 | db-urls = ["https://github.com/rustsec/advisory-db"] 16 | 17 | # https://wiki.gentoo.org/wiki/License_groups/GPL-COMPATIBLE 18 | [licenses] 19 | unused-allowed-license = "warn" 20 | confidence-threshold = 0.8 21 | allow = [ 22 | "GPL-3.0", 23 | "MIT", 24 | "Apache-2.0", 25 | "Apache-2.0 WITH LLVM-exception", 26 | "Zlib", 27 | "MPL-2.0", 28 | "Unicode-DFS-2016", 29 | ] 30 | 31 | [bans] 32 | multiple-versions = "warn" 33 | 34 | [sources] 35 | unknown-registry = "deny" 36 | unknown-git = "deny" 37 | 38 | [sources.allow-org] 39 | github = ["mahor1221"] 40 | -------------------------------------------------------------------------------- /docs/redshift-contributing.md: -------------------------------------------------------------------------------- 1 | 2 | Building from git clone 3 | ----------------------- 4 | 5 | ``` shell 6 | $ ./bootstrap 7 | $ ./configure 8 | ``` 9 | 10 | The bootstrap script will use autotools to set up the build environment 11 | and create the `configure` script. 12 | 13 | Use `./configure --help` for options. Use `--prefix` to make an install in 14 | your home directory. This is necessary to test python scripts. The systemd 15 | user unit directory should be set to avoid writing to the system location. 16 | 17 | Systemd will look for the unit files in `~/.config/systemd/user` so this 18 | directory can be used as a target if the unit files will be used. Otherwise 19 | the location can be set to `no` to disable the systemd files. 20 | 21 | Example: 22 | 23 | ``` shell 24 | $ ./configure --prefix=$HOME/redshift/root \ 25 | --with-systemduserunitdir=$HOME/.config/systemd/user 26 | ``` 27 | 28 | Now, build the files: 29 | 30 | ``` shell 31 | $ make 32 | ``` 33 | 34 | The main redshift program can be run at this point. To install to the 35 | prefix directory run: 36 | 37 | ``` shell 38 | $ make install 39 | ``` 40 | 41 | You can now run the python script. Example: 42 | 43 | ``` shell 44 | $ $HOME/redshift/root/bin/redshift-gtk 45 | ``` 46 | 47 | 48 | Dependencies 49 | ------------ 50 | 51 | * autotools, gettext 52 | * intltool, libtool 53 | * libdrm (Optional, for DRM support) 54 | * libxcb, libxcb-randr (Optional, for RandR support) 55 | * libX11, libXxf86vm (Optional, for VidMode support) 56 | * Glib 2 (Optional, for GeoClue2 support) 57 | 58 | * python3, pygobject, pyxdg (Optional, for GUI support) 59 | * appindicator (Optional, for Ubuntu-style GUI status icon) 60 | 61 | Ubuntu users will find all these dependencies in the packages listed in ``.travis.yml``. 62 | 63 | 64 | Coding style for C code 65 | ----------------------- 66 | 67 | Redshift follows roughly the Linux coding style 68 | . Some specific rules to 69 | note are: 70 | 71 | * Lines should not be longer than 80 characters in new code. If lines are 72 | longer than this the code could likely be improved by moving some parts to a 73 | smaller function. 74 | * All structures are typedef'ed. 75 | * Avoid Yoda conditions; they make the logic unnecessarily hard to comprehend. 76 | * Avoid multiline if-statements without braces; either use a single line or add 77 | the braces. 78 | * Use only C-style comments (`/* */`). 79 | 80 | 81 | Creating a pull request 82 | ----------------------- 83 | 84 | 1. Create a topic branch for your specific changes. You can base this off the 85 | master branch or a specific version tag if you prefer (`git co -b topic master`). 86 | 2. Create a commit for each logical change on the topic branch. The commit log 87 | must contain a one line description (max 80 chars). If you cannot describe 88 | the commit in 80 characters you should probably split it up into multiple 89 | commits. The first line can be followed by a blank line and a longer 90 | description (split lines at 80 chars) for more complex commits. If the commit 91 | fixes a known issue, mention the issue number in the first line (`Fix #11: 92 | ...`). 93 | 3. The topic branch itself should tackle one problem. Feel free to create many 94 | topic branches and pull requests if you have many different patches. Putting 95 | them into one branch makes it harder to review the code. 96 | 4. Push the topic branch to Github, find it on github.com and create a pull 97 | request to the master branch. If you are making a bug fix for a specific 98 | release you can create a pull request to the release branch instead 99 | (e.g. `release-1.9`). 100 | 5. Discussion will ensue. If you are not prepared to partake in the discussion 101 | or further improve your patch for inclusion, please say so and someone else 102 | may be able to take on responsibility for your patch. Otherwise we will 103 | assume that you will be open to criticism and suggestions for improvements 104 | and that you will take responsibility for further improving the patch. You 105 | can add further commits to your topic branch and they will automatically be 106 | added to the pull request when you push them to Github. 107 | 6. You may be asked to rebase the patch on the master branch if your patch 108 | conflicts with recent changes to the master branch. However, if there is no 109 | conflict, there is no reason to rebase. Please do not merge the master back 110 | into your topic branch as that will convolute the history unnecessarily. 111 | 7. Finally, when your patch has been refined, you may be asked to squash small 112 | commits into larger commits. This is simply so that the project history is 113 | clean and easy to follow. Remember that each commit should be able to stand 114 | on its own, be able to compile and function normally. Commits that fix a 115 | small error or adds a few comments to a previous commit should normally just 116 | be squashed into that larger commit. 117 | 118 | If you want to learn more about the Git branching model that we use please see 119 | but note that we use 120 | the `master` branch as `develop`. 121 | 122 | 123 | Contributing translations 124 | ------------------------- 125 | 126 | You can contribute translations directly at 127 | [Launchpad Translations for Redshift](https://translations.launchpad.net/redshift). 128 | Updated translations will be pulled back into the `po` files on Github 129 | before a release is made. 130 | 131 | 132 | Creating a new release 133 | ---------------------- 134 | 135 | 1. Select a commit in master to branch from, or if making a bugfix release 136 | use previous release tag as base (e.g. for 1.9.1 use 1.9 as base) 137 | 2. Create release branch `release-X.Y` 138 | 3. Apply any bugfixes for release 139 | 4. Import updated translations from launchpad and commit. Remember to update 140 | `po/LINGUAS` if new languages were added 141 | 5. Update version in `configure.ac` and create entry in NEWS 142 | 6. Run `make distcheck` 143 | 7. Commit and tag release (`vX.Y` or `vX.Y.Z`) 144 | 8. Push tag to Github and also upload source dist file to Github 145 | 146 | Also remember to check before release that 147 | 148 | * Windows build is ok 149 | * Build files for distributions are updated 150 | 151 | 152 | Build Fedora RPMs 153 | ----------------- 154 | 155 | Run `make dist-xz` and copy the `.tar.xz` file to `~/rpmbuild/SOURCES`. Then run 156 | 157 | ``` shell 158 | $ rpmbuild -ba contrib/redshift.spec 159 | ``` 160 | 161 | If successful this will place RPMs in `~/rpmbuild/RPMS`. 162 | 163 | 164 | Cross-compile for Windows 165 | ------------------------- 166 | 167 | Install MinGW and run `configure` using the following command line. Use 168 | `i686-w64-mingw32` as host for 32-bit builds. 169 | 170 | ``` shell 171 | $ ./configure --disable-drm --disable-randr --disable-vidmode --enable-wingdi \ 172 | --disable-quartz --disable-geoclue2 --disable-corelocation --disable-gui \ 173 | --disable-ubuntu --host=x86_64-w64-mingw32 174 | ``` 175 | 176 | 177 | Notes 178 | ----- 179 | * verbose flag is (currently) only held in redshift.c; thus, write all 180 | verbose messages there. 181 | -------------------------------------------------------------------------------- /docs/redshift-design.md: -------------------------------------------------------------------------------- 1 | This is a document describing how redshift works. It might be useful 2 | if the normal docs don't answer a question, or when you want to hack 3 | on the code. 4 | 5 | 6 | Programs 7 | ======== 8 | 9 | redshift 10 | -------- 11 | 12 | redshift is a program written in C that tries to figure out the user's 13 | location during startup, and then goes into a loop setting the display 14 | gamma according to the time of day every couple seconds or minutes 15 | (details?). 16 | 17 | On systems that support signals, it reacts to the SIGUSR1 signal by 18 | switching to day/night immediately, and when receiving SIGINT or 19 | SIGTERM, it restores the screen gamma (to 6500K). 20 | 21 | Redshift knows short and long transitions, short transitions being 22 | used at start and when reacting to signals. Short transitions take 23 | about 10 seconds; long transitions about 50 minutes. 24 | 25 | Once running, redshift currently doesn't check location providers 26 | again. 27 | 28 | 29 | redshift-gtk 30 | ------------ 31 | 32 | redshift-gtk is a small program written in Python that shows a status 33 | icon (what is an appindicator versus a GTK status icon?) (does it 34 | change the icon according to internal program state of redshift? 35 | doesn't seem so) and run an instance of the "redshift" program, and 36 | will send it SIGUSR1 each time the user clicks the icon. 37 | 38 | 39 | Alternative Features 40 | ==================== 41 | 42 | Redshift interacts with the rest of the system in two ways: reading 43 | the location, and setting the gamma. Both can be done in different 44 | ways, and so for both areas there are configure options to 45 | enable/disable compilation of the various methods. ./configure --help 46 | shows more about what parts of the program that can be conditionally 47 | compiled. 48 | 49 | NOTE: some features have to be disabled explicitly, like 50 | --disable-gnome-clock to prevent the gnome-clock code from being built 51 | in. 52 | 53 | The two groups of features shall be called: "location providers" and 54 | "adjustment methods". 55 | 56 | These are probably not the best names for these things but at least 57 | I've been mostly consistent with the naming throughout the source code 58 | (I hope). 59 | 60 | First adjustment methods: There is "randr" which is the preferred 61 | because it has support for multiple outputs per X screen which is lacking 62 | in "vidmode". Both are APIs in the X server that allow for manipulation 63 | of gamma ramps, which is what Redshift uses to change the screen color 64 | temperature. There's also "wingdi" which is for the Windows version, 65 | and "drm" which allows manipulation of gamma ramps in a TTY in Linux. 66 | 67 | Then there are location providers: "manual", "geoclue2" and "corelocation". 68 | Some time ago there was only one way to specify the 69 | location which had to be done manually with the argument "-l LAT:LON". 70 | Then later, automatic "location providers" were added and the syntax 71 | had to be changed to "-l PROVIDER:OPTIONS" where OPTIONS are arguments 72 | specific to the provider. But to make people less confused about the 73 | change I decided to still support the "-l LAT:LON" syntax, so if the 74 | PROVIDER is a number, the whole thing is parsed as LAT:LON. You could 75 | run redshift with "-l manual:lat=55:lon=12" and get the same effect as 76 | "-l 55:12". 77 | 78 | So there are currently two automatic location providers "gnome-clock" 79 | and "geoclue". From the beginning I was looking for a way to get the 80 | location automatically (from e.g. GPS) and Geoclue seemed like a good 81 | idea, but upon closer investigation it turned out to be horribly 82 | unstable. At this time GNOME had a clock applet which was present by 83 | default (at least in Ubuntu) that allowed the user to set a home town. 84 | This setting was registered in the gconf key 85 | /apps/panel/applets/clock_screen*/prefs/cities. 86 | The idea was to use this information until Geoclue had become more 87 | stable. To me, it always was a hack. Now that the Clock applet has 88 | gone (at least in Ubuntu) the "gnome-clock" makes little sense and 89 | causes a lot of trouble, so I really want to get rid of it as soon as 90 | possible. The problem is that Geoclue is still problematic for some 91 | people. 92 | 93 | Lastly, there's the support for configuration files for which there's 94 | no real documentation, but all the options that can be set on the 95 | command line can also be set in the config file. 96 | -------------------------------------------------------------------------------- /docs/redshift-news.md: -------------------------------------------------------------------------------- 1 | News 2 | ==== 3 | 4 | v1.12 (2018-05-20) 5 | ------------------ 6 | - Change location providers to allow updates. GeoClue and CoreLocation now 7 | provide continuous location updates. 8 | - Allow time-based configuration i.e. setting the redness effect based on time 9 | of day instead of based on the elevation of the sun. See the man page for 10 | more information. 11 | - Now looks for the configuration file in `~/.config/redshift/redshift.conf` 12 | (or `${XDG_CONFIG_HOME}/redshift/redshift.conf`) if `$XDG_CONFIG_HOME` is 13 | set. The old location at `~/.config/redshift.conf` is deprecated but 14 | still searched as a fall back. 15 | - Run hooks when enabling/disabling Redshift. 16 | - Default temperatures changed to 6500K during daytime and 4500K during night. 17 | - With `randr`, allow multiple but not all CRTCs to be redshifted. 18 | - Removes deprecated original GeoClue location provider (use GeoClue 2 19 | instead). 20 | - The option for enabling the short fade between color effects is now called 21 | `fade` instead of `transition` in the configuration file. The term transition 22 | caused a lot of confusion about what this option does (the old option still 23 | works but is deprecated). 24 | - The `preserve` option is enabled by default for `vidmode`, `randr`, Windows 25 | (`w32gdi`) and macOS (`quartz`). The option is now controlled by the `-P` 26 | command line option. 27 | - Work around issue where Windows adjustments sometimes fail. 28 | - Install AppArmor profile. 29 | - quartz: Fix incorrect use of display identifier. 30 | - Various bug fixes and updated translations. 31 | 32 | v1.11 (2016-01-02) 33 | ------------------ 34 | - Add option `preserve` for gamma adjustment methods (`randr`, `vidmode`, 35 | `quartz`, `w32gdi`) to apply redness on top of current gamma correction. 36 | - Fix #158: Add redshift.desktop file to resolve an issue where Geoclue2 37 | would not allow redshift to obtain the current location (Laurent Bigonville) 38 | - Fix #263: Make sure that the child process is terminated when redshift-gtk 39 | exits. 40 | - Fix #284: A sample configuration file has been added to the distribution 41 | tarball. 42 | - Fix warning message in redshift-gtk that is some cases caused redshift-gtk 43 | to fail (#271) (Christian Stadelmann, Javier Cantero) 44 | - Fix #174: Use nanosleep() for sleeping to avoid high CPU load on platforms 45 | (NetBSD, ...) with limitations in usleep() (Piotr Meyer) 46 | - Various updates to man page and translations. 47 | 48 | 49 | v1.10 (2015-01-04) 50 | ------------------ 51 | * Fix #80: Add Geoclue2 location provider. 52 | * Add CoreLocation (OSX) location provider and Quartz (OSX) gamma 53 | adjustment method. 54 | * Add hooks for user actions on period switch. 55 | * Be less verbose when color values/period did not change. 56 | * Add config setting to set gamma separately for day/night. 57 | * Add support for custom transition start and end elevation (Mattias 58 | Andrée). 59 | * redshift-gtk: Show errors from child process in a dialog. 60 | * Fix #95: Add AppData file for package managers. 61 | * Use gettimeofday if POSIX timers not available (add support for 62 | OSX). 63 | * Fix #41: Do not jump to 0 % or 100 % when changing direction of 64 | transition (Mattias Andrée). 65 | * redshift-gtk: Relay USR1 signal to redshift process. 66 | * redshift-gtk: Notify desktop about startup completion. 67 | * Fix: systemd unit files were built from the wrong source. 68 | * Fix #90: Print N/S and E/W in the location (Mattias Andrée). 69 | * Fix #112: redshift-gtk: Do not buffer lines from child indefinitely. 70 | * Fix #105: Limit decimals in displayed location to two. 71 | * Update dependencies listed in HACKING.md (emilf, Kees Hink). 72 | * Fix: Make desktop file translatable. 73 | * Add Travis CI build script. 74 | 75 | v1.9.1 (2014-04-20) 76 | ------------------- 77 | * Fix: Do not distribute redshift-gtk, only redshift-gtk.in. 78 | * Fix: Geoclue support should pull in Glib as dependency. 79 | * geoclue: Fix segfault when error is NULL (Mattias Andrée). 80 | * geoclue: Set DISPLAY=:0 to work around issue when outside X 81 | (Mattias Andrée). 82 | * redshift-gtk: Fix crash when toggling state using the status icon. 83 | * redshift-gtk: Fix line splitting logic (Maks Verver). 84 | 85 | v1.9 (2014-04-06) 86 | ----------------- 87 | * Use improved color scheme provided by Ingo Thies. 88 | * Add drm driver which will apply adjustments on linux consoles 89 | (Mattias Andrée). 90 | * Remove deprecated GNOME clock location provider. 91 | * Set proc title for redshift-gtk (Linux/BSD) (Philipp Hagemeister). 92 | * Show current temperature, location and status in GUI. 93 | * Add systemd user unit files so that redshift can be used with 94 | systemd as a session manager (Henry de Valence). 95 | * Use checkbox to toggle Redshift in GUI (Mattias Andrée). 96 | * Gamma correction is applied after brightness and temperature 97 | (Mattias Andrée). 98 | * Use XDG Base Directory Specification when looking for configuration 99 | file (Mattias Andrée). 100 | * Load config from %LOCALAPPDATA%\redshift.conf on Windows (TingPing). 101 | * Add RPM spec for Fedora in contrib. 102 | * redshift-gtk has been ported to Python3 and new PyGObject bindings 103 | for Python. 104 | 105 | v1.8 (2013-10-21) 106 | ----------------- 107 | * IMPORTANT: gtk-redshift has changed name to redshift-gtk. 108 | * Fix crash when starting geoclue provider. (Thanks to Maks Verver) 109 | * Fix slight flicker int gamme ramp values (Sean Hildebrand) 110 | * Add redshift-gtk option to suspend for a short time period (Jendrik Seipp). 111 | * Add print mode (prints parameters and exits) by Vincent Breitmoser. 112 | * Set buffering on stdout and stderr to line-buffered. 113 | * Allow separate brightness for day and night (Olivier Fabre and Jeremy Erickson). 114 | * Fix various crashes/bugs/typos (Benjamin Kerensa and others) 115 | 116 | v1.7 (2011-07-04) 117 | ----------------- 118 | * Add Geoclue location provider by Mathieu Trudel-Lapierre. 119 | * Allow brightness to be adjusted (-b). 120 | * Provide option to set color temperature directly (Joe Hillenbrand). 121 | * Add option to show program version (-V). 122 | * Add configure.ac option to install ubuntu icons. They will no longer be 123 | installed by default (Francesco Marella). 124 | * config: Look in %userprofile%/.config/redshift.conf on windows platform. 125 | * Fix: w32gdi: Obtain a new DC handle on every adjustment. This fixes a bug 126 | where redshift stops updating the screen. 127 | 128 | v1.6 (2010-10-18) 129 | ----------------- 130 | * Support for optional configuration file (fixes #590722). 131 | * Add man page for redshift written by Andrew Starr-Bochicchio (fixes #582196). 132 | * Explain in help output that 6500K is the neutral color temperature 133 | (fixes #627113). 134 | * Fix: Handle multiple instances of the GNOME clock applet; contributed by 135 | Francesco Marella (fixes #610860). 136 | * Fix: Redshift crashes when VidMode fails (fixes #657451). 137 | * Fix: Toggle menu item should not be of class gtk.ImageMenuItem 138 | (fixes #620355). 139 | * New translations and translation updates: Lithuanian (Aurimas Fišeras); 140 | Brazilian Portuguese (Matteus Sthefano Leite da Silva); 141 | Finnish (Jani Välimaa); Italian (Simone Sandri); French (Emilien Klein); 142 | Russian (Anton Chernyshov). 143 | 144 | v1.5 (2010-08-18) 145 | ----------------- 146 | * New ubuntu-mono-dark icons that fit better with the color guidelines. 147 | Contributed by aleth. 148 | * Improve GNOME location provider (patch by Gabriel de Perthuis). 149 | * Application launcher and autostart feature contributed by Francesco Marella. 150 | * Translation updates: Basque (Ibai Oihanguren); Chinese (Jonathan Lumb); 151 | French (Hangman, XioNoX); German (Jan-Christoph Borchardt); Hebrew 152 | (dotancohen); Spanish (Fernando Ossandon). 153 | 154 | v1.4.1 (2010-06-15) 155 | ------------------- 156 | * Include Ubuntu Mono icons by Joern Konopka. 157 | * Fix: Toggle icon in statusicon.py like appindicator already does. 158 | * Translation updates: Spanish (Fernando Ossandon), Russian (Чистый) 159 | 160 | v1.4 (2010-06-13) 161 | ----------------- 162 | * Command line options for color adjustment methods changed. Procedure for 163 | setting specific screen (-s) or CRTC (-c) changed. See `redshift -h` for 164 | more information. 165 | * Automatically obtain the location from the GNOME Clock applet if possible. 166 | * Add application indicator GUI (by Francesco Marella) (fixes #588086). 167 | * Add reset option (-x) that removes any color adjustment applied. Based on 168 | patch by Dan Helfman (fixes #590777). 169 | * `configure` options for GUI changed; see `configure --help` for more 170 | information. 171 | * New translations: 172 | - German (Jan-Christoph Borchardt) 173 | - Italian (Andrea Amoroso) 174 | - Czech (clever_fox) 175 | - Spanish (Fernando Ossandon) 176 | - Finnish (Ilari Oras) 177 | 178 | v1.3 (2010-05-12) 179 | ----------------- 180 | * Allow adjusting individual CRTCs when using RANDR. Contributed by 181 | Alexandros Frantzis. 182 | * Add WinGDI method for gamma adjustments on Windows platform. 183 | * Compile with mingw (tested with cross compiler on ubuntu build system). 184 | 185 | v1.2 (2010-02-12) 186 | ----------------- 187 | * Native language support: Danish and russian translations included in 188 | this release. Thanks goes to Gregory Petrosyan for the russian 189 | translation. 190 | 191 | v1.1 (2010-01-14) 192 | ----------------- 193 | * Provide a GTK status icon (tray icon) for redshift with the 194 | gtk-redshift program. 195 | 196 | v1.0 (2010-01-09) 197 | ----------------- 198 | * Temporarily disable/enable when USR1 signal is received. 199 | 200 | v0.4 (2010-01-07) 201 | ----------------- 202 | * Restore gamma ramps on program exit. 203 | 204 | v0.3 (2009-12-28) 205 | ----------------- 206 | * Continuously adjust color temperature. One shot mode can be selected 207 | with a command line switch. 208 | * Allow selection of X screen to apply adjustments to. 209 | 210 | v0.2 (2009-12-23) 211 | ----------------- 212 | * Add a different method for setting the gamma ramps. It uses the 213 | VidMode extension. 214 | 215 | v0.1 (2009-11-04) 216 | ----------------- 217 | * Initial release. 218 | -------------------------------------------------------------------------------- /docs/redshift-readme.md: -------------------------------------------------------------------------------- 1 | 2 | Redshift 3 | ======== 4 | 5 | Redshift adjusts the color temperature of your screen according to 6 | your surroundings. This may help your eyes hurt less if you are 7 | working in front of the screen at night. 8 | 9 | README versions: [latest](https://github.com/jonls/redshift/blob/master/README.md) | [1.12](https://github.com/jonls/redshift/blob/v1.12/README.md) | [1.11](https://github.com/jonls/redshift/blob/v1.11/README.md) 10 | 11 | ![Redshift logo](http://jonls.dk/assets/redshift-icon-256.png) 12 | 13 | Run `redshift -h` for help on command line options. You can run the program 14 | as `redshift-gtk` instead of `redshift` for a graphical status icon. 15 | 16 | * Project page: https://github.com/jonls/redshift 17 | 18 | Build status 19 | ------------ 20 | 21 | [![Build Status](https://travis-ci.org/jonls/redshift.svg?branch=master)](https://travis-ci.org/jonls/redshift) 22 | [![Build Status](https://ci.appveyor.com/api/projects/status/github/jonls/redshift?branch=master&svg=true)](https://ci.appveyor.com/project/jonls/redshift) 23 | 24 | Technical Details (Gamma Ramps) 25 | ------------------------------- 26 | 27 | Redshift applies a redness effect to the graphical display. The intensity of the redness can be customized and 28 | scheduled to only be applied at night or to be applied with more intensity at night. 29 | 30 | Redshift uses the gamma correction ramps to apply this effect which has traditionally been the only 31 | way of applying a color effect to the display. Note that this is really a hack to work around the absence of a 32 | standardized way of applying color effects, and it is resulting in several issues some of which are explained in 33 | the _FAQ_ section below. As long as Redshift is using gamma ramps, many of these issues are impossible to solve properly 34 | in Redshift. 35 | 36 | Since Redshift was first created, many desktop environments have adopted similar functionality as an integrated 37 | component: 38 | 39 | - GNOME: [Night Light](https://www.gnome.org/news/2017/03/gnome-3-24-released/attachment/night-light/) 40 | - Plasma: Night Color 41 | - Windows: [Night Light](https://support.microsoft.com/en-us/help/4027563/windows-10-set-your-display-for-night-time) 42 | - macOS: Night Shift 43 | 44 | Using the features integrated into the desktop environments avoids many of the issues with gamma ramps which is why 45 | these implementations should generally be favored over Redshift. On the other hand, Redshift may offer some additional 46 | flexibility that is not available in the integrated features. 47 | 48 | FAQ 49 | --- 50 | 51 | ### How do I install Redshift? 52 | 53 | Use the packages provided by your distribution, e.g. for Ubuntu: 54 | `apt-get install redshift` or `apt-get install redshift-gtk`. For developers, 55 | please see _Building from source_ and _Latest builds from master branch_ below. 56 | 57 | ### How do I setup a configuration file? 58 | 59 | A configuration file is not required but is useful for saving custom 60 | configurations and manually defining the location in case of issues with the 61 | automatic location provider. An example configuration can be found in 62 | [redshift.conf.sample](redshift.conf.sample). 63 | 64 | The configuration file should be saved in the following location depending on 65 | the platform: 66 | 67 | - Linux/macOS: `~/.config/redshift/redshift.conf` (if the environment variable `XDG_CONFIG_HOME` is undefined) or `${XDG_CONFIG_HOME}/redshift/redshift.conf` (if `XDG_CONFIG_HOME` is defined). 68 | - Windows: Put `redshift.conf` in `%USERPROFILE%\AppData\Local\` 69 | (aka `%localappdata%`). 70 | 71 | ### Where can I find my coordinates to put in the configuration file? 72 | 73 | There are multiple web sites that provide coordinates for map locations, for 74 | example clicking anywhere on Google Maps will bring up a box with the 75 | coordinates. Remember that longitudes in the western hemisphere (e.g. the 76 | Americas) must be provided to Redshift as negative numbers. 77 | 78 | ### Why does GeoClue fail with access denied error? 79 | 80 | It is possible that the location services have been disabled completely. The 81 | check for this case varies by desktop environment. For example, in GNOME the 82 | location services can be toggled in Settings > Privacy > Location Services. 83 | 84 | If this is not the case, it is possible that Redshift has been improperly 85 | installed or not been given the required permissions to obtain location 86 | updates from a system administrator. See 87 | https://github.com/jonls/redshift/issues/318 for further discussion on this 88 | issue. 89 | 90 | ### Why doesn't Redshift work on my Chromebook/Raspberry Pi? 91 | 92 | Certain video drivers do not support adjustable gamma ramps. In some cases 93 | Redshift will fail with an error message, but other drivers silently ignore 94 | adjustments to the gamma ramp. 95 | 96 | ### Why doesn't Redshift change the backlight when I use the brightness option? 97 | 98 | Redshift has a brightness adjustment setting but it does not work the way most 99 | people might expect. In fact it is a fake brightness adjustment obtained by 100 | manipulating the gamma ramps which means that it does not reduce the backlight 101 | of the screen. Preferably only use it if your normal backlight adjustment is 102 | too coarse-grained. 103 | 104 | ### Why doesn't Redshift work on Wayland (e.g. Fedora 25)? 105 | 106 | The Wayland protocol does not support Redshift. There is currently no way for 107 | Redshift to adjust the color temperature in Wayland. 108 | 109 | Instead, you can use: 110 | 111 | - In GNOME Shell: Settings → Display Settings → Night Light 112 | - In Plasma: System Settings → Display and Monitor → Night Color 113 | 114 | ### Why doesn't Redshift work on Ubuntu with Mir enabled? 115 | 116 | Mir does not support Redshift. 117 | 118 | ### When running as a systemd service, redshift fails to connect to the graphical environment 119 | 120 | You need to export your environment variables when your window manager or 121 | compositor start up. Typically, you want to run this as part of its startup: 122 | 123 | systemctl --user import-environment; systemctl --user start graphical-session.target 124 | 125 | See your compositor's (or window manager's) documentation for further details 126 | of setting up the systemd user session. 127 | 128 | Related issues: [#753](https://github.com/jonls/redshift/pull/753). 129 | 130 | ### The redness effect is applied during the day instead of at night. Why? 131 | 132 | This usually happens to users in America when the longitude has been set in the 133 | configuration file to a positive number. Longitudes in the western hemisphere 134 | should be provided as negative numbers (e.g. New York City is at approximately 135 | latitude/longitude 41, -74). 136 | 137 | ### Why does the redness effect occasionally switch off for a few seconds? 138 | 139 | Redshift uses the gamma ramps of the graphics driver to apply the redness 140 | effect but Redshift cannot block other applications from also changing the 141 | gamma ramps. Some applications (particularly games and video players) will 142 | reset the gamma ramps. After a few seconds Redshift will kick in again. There 143 | is no way for Redshift to prevent this from happening. 144 | 145 | ### Why does the redness effect continuously flicker? 146 | 147 | You may have multiple instances of Redshift running simultaneously. Make sure 148 | that only one instance is running for the display where you are seeing the 149 | flicker. 150 | 151 | ### Why doesn't Redshift change the color of the mouse cursor? 152 | 153 | Mouse cursors are usually handled separately by the graphics hardware and is 154 | not affected by gamma ramps. Some graphics drivers can be configured to use 155 | software cursors instead. 156 | 157 | ### I have an issue with Redshift but it was not mentioned in this FAQ. What do I do? 158 | 159 | Please go to [the issue tracker](https://github.com/jonls/redshift/issues) and 160 | check if your issue has already been reported. If not, please open a new issue 161 | describing you problem. 162 | 163 | Latest builds from master branch 164 | -------------------------------- 165 | 166 | - [Ubuntu PPA](https://launchpad.net/~dobey/+archive/ubuntu/redshift-daily/+packages) (`sudo add-apt-repository ppa:dobey/redshift-daily`) 167 | - [Windows x86_64](https://ci.appveyor.com/api/projects/jonls/redshift/artifacts/redshift-windows-x86_64.zip?branch=master&job=Environment%3A+arch%3Dx86_64&pr=false) 168 | - [Windows x86](https://ci.appveyor.com/api/projects/jonls/redshift/artifacts/redshift-windows-i686.zip?branch=master&job=Environment%3A+arch%3Di686&pr=false) 169 | 170 | Contributing / Building from source 171 | ----------------------------------- 172 | 173 | See the file [CONTRIBUTING](CONTRIBUTING.md) for more details. 174 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 79 2 | -------------------------------------------------------------------------------- /scripts/generate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "generate" 3 | version = "0.0.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | [build-dependencies] 8 | anyhow = "1.0" 9 | clap = { version = "*", features = ["string"] } 10 | clap_complete = "4.5.6" 11 | clap_mangen = "0.2.22" 12 | reddish-shift = { path = "../../" } 13 | 14 | [lints] 15 | workspace = true 16 | -------------------------------------------------------------------------------- /scripts/generate/build.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unwrap_used)] 2 | use anyhow::Result; 3 | use clap_complete::{generate_to, Shell}; 4 | use clap_mangen::Man; 5 | use reddish_shift::cli_args_command; 6 | use std::{env, fs, path::PathBuf}; 7 | 8 | fn main() -> Result<()> { 9 | const NAME: &str = "reddish-shift"; 10 | let out = env::var_os("OUT_DIR").unwrap(); 11 | let target = PathBuf::from(&out).ancestors().nth(3).unwrap().to_owned(); 12 | let mut cmd = cli_args_command(); 13 | 14 | // generate shell completion scripts 15 | use Shell::*; 16 | let path = target.join("completion"); 17 | fs::remove_dir_all(&path).unwrap_or_default(); 18 | fs::create_dir(&path)?; 19 | for sh in [Bash, Elvish, Fish, Zsh, PowerShell] { 20 | generate_to(sh, &mut cmd, NAME, &out)?; 21 | } 22 | 23 | for file in fs::read_dir(&out)? { 24 | let f = file?.path(); 25 | fs::rename(&f, path.join(f.file_name().unwrap()))?; 26 | } 27 | 28 | // generate man pages 29 | let path = target.join("man1"); 30 | fs::remove_dir_all(&path).unwrap_or_default(); 31 | fs::create_dir(&path)?; 32 | let mut buffer: Vec = Default::default(); 33 | for subcmd in cmd.get_subcommands() { 34 | let subcmd_name = format!("{NAME}-{}", subcmd.get_name()); 35 | Man::new(subcmd.clone().name(&subcmd_name)).render(&mut buffer)?; 36 | fs::write(path.join(format!("{subcmd_name}.1")), &buffer)?; 37 | buffer.clear(); 38 | } 39 | Man::new(cmd).render(&mut buffer)?; 40 | fs::write(path.join(format!("{NAME}.1")), buffer)?; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /scripts/generate/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/calc_colorramp.rs: -------------------------------------------------------------------------------- 1 | /* calc_colorramp.rs -- Color temperature calculation 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2013-2014 Jon Lund Steffensen 6 | Copyright (c) 2013 Ingo Thies 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | use crate::types::ColorSettings; 23 | use std::ops::{Deref, DerefMut}; 24 | 25 | #[cfg(unix_without_macos)] 26 | #[derive(Debug, Clone)] 27 | pub struct GammaRamps(pub [Vec; 3]); 28 | 29 | #[allow(unused)] 30 | #[derive(Debug, Clone)] 31 | pub struct GammaRampsFloat(pub [Vec; 3]); 32 | 33 | #[cfg(windows)] 34 | #[derive(Debug, Clone)] 35 | pub struct GammaRampsWin32(pub Box<[[u16; SIZE]; 3]>); 36 | 37 | // A macro is used to prevent repetition. The same effect can be achieved with 38 | // Iterator traits, but this is easier to read. 39 | #[cfg(any(unix_without_macos, windows))] 40 | macro_rules! colorramp_fill { 41 | ($self:ident, $setting:ident) => { 42 | let white_point = approximate_white_point($setting); 43 | let a = (u16::MAX as u32 + 1) as f64; 44 | let f = |y: u16, c: usize| -> u16 { 45 | let r = y as f64 / a * *$setting.brght * white_point[c]; 46 | let r = r.powf(1.0 / $setting.gamma[c]) * a; 47 | r as u16 48 | }; 49 | 50 | for i in 0..$self[0].len() { 51 | $self[0][i] = f($self[0][i], 0); 52 | $self[1][i] = f($self[1][i], 1); 53 | $self[2][i] = f($self[2][i], 2); 54 | } 55 | }; 56 | } 57 | 58 | #[cfg(unix_without_macos)] 59 | impl GammaRamps { 60 | // used in vidmode and randr 61 | pub fn new(ramp_size: u32) -> Self { 62 | // Initialize gamma ramps to pure state 63 | // if ramp_size == 1024 => ramps == [[0, 64, 128, 192, ..], ..] 64 | let a = (u16::MAX as u32 + 1) as f64; 65 | let v = (0..ramp_size) 66 | .map(|i| (i as f64 / ramp_size as f64 * a) as u16) 67 | .collect::>(); 68 | Self([v.clone(), v.clone(), v]) 69 | } 70 | 71 | pub fn colorramp_fill(&mut self, setting: &ColorSettings) { 72 | colorramp_fill!(self, setting); 73 | } 74 | } 75 | 76 | #[cfg(windows)] 77 | impl GammaRampsWin32 { 78 | pub fn new() -> Self { 79 | // Initialize gamma ramps to pure state 80 | // if ramp_size == 1024 => ramps == [[0, 64, 128, 192, ..], ..] 81 | let a = (u16::MAX as u32 + 1) as f64; 82 | let mut ramp = [0; SIZE]; 83 | #[allow(clippy::needless_range_loop)] 84 | for i in 0..SIZE { 85 | ramp[i] = (i as f64 / SIZE as f64 * a) as u16; 86 | } 87 | Self(Box::new([ramp; 3])) 88 | } 89 | 90 | pub fn colorramp_fill(&mut self, setting: &ColorSettings) { 91 | colorramp_fill!(self, setting); 92 | } 93 | } 94 | 95 | impl GammaRampsFloat { 96 | #![allow(dead_code)] 97 | pub fn colorramp_fill(&mut self, setting: &ColorSettings) { 98 | let white_point = approximate_white_point(setting); 99 | let f = |y: f64, c: usize| -> f64 { 100 | let r = y * *setting.brght * white_point[c]; 101 | r.powf(1.0 / setting.gamma[c]) 102 | }; 103 | 104 | for i in 0..self[0].len() { 105 | self[0][i] = f(self[0][i], 0); 106 | self[1][i] = f(self[1][i], 1); 107 | self[2][i] = f(self[2][i], 2); 108 | } 109 | } 110 | } 111 | 112 | fn approximate_white_point(setting: &ColorSettings) -> [f64; 3] { 113 | let alpha = (*setting.temp % 100) as f64 / 100.0; 114 | let temp_index = (*setting.temp - 1000) as usize / 100; 115 | interpolate_color( 116 | alpha, 117 | &BLACKBODY_COLOR[temp_index], 118 | &BLACKBODY_COLOR[temp_index + 3], 119 | ) 120 | } 121 | 122 | fn interpolate_color(alpha: f64, c1: &[f64], c2: &[f64]) -> [f64; 3] { 123 | [ 124 | (1.0 - alpha) * c1[0] + alpha * c2[0], 125 | (1.0 - alpha) * c1[1] + alpha * c2[1], 126 | (1.0 - alpha) * c1[2] + alpha * c2[2], 127 | ] 128 | } 129 | 130 | // Read NOTE in src/config.rs 131 | #[cfg(unix_without_macos)] 132 | impl Deref for GammaRamps { 133 | type Target = [Vec; 3]; 134 | fn deref(&self) -> &Self::Target { 135 | &self.0 136 | } 137 | } 138 | 139 | impl Deref for GammaRampsFloat { 140 | type Target = [Vec; 3]; 141 | fn deref(&self) -> &Self::Target { 142 | &self.0 143 | } 144 | } 145 | 146 | #[cfg(windows)] 147 | impl Deref for GammaRampsWin32 { 148 | type Target = [[u16; SIZE]; 3]; 149 | fn deref(&self) -> &Self::Target { 150 | &self.0 151 | } 152 | } 153 | 154 | #[cfg(unix_without_macos)] 155 | impl DerefMut for GammaRamps { 156 | fn deref_mut(&mut self) -> &mut Self::Target { 157 | &mut self.0 158 | } 159 | } 160 | 161 | impl DerefMut for GammaRampsFloat { 162 | fn deref_mut(&mut self) -> &mut Self::Target { 163 | &mut self.0 164 | } 165 | } 166 | 167 | #[cfg(windows)] 168 | impl DerefMut for GammaRampsWin32 { 169 | fn deref_mut(&mut self) -> &mut Self::Target { 170 | &mut self.0 171 | } 172 | } 173 | 174 | // Whitepoint values for temperatures at 100K intervals. 175 | // These will be interpolated for the actual temperature. 176 | // This table was provided by Ingo Thies, 2013. See 177 | // the file doc/redshift-colorramp.md for more information. 178 | const BLACKBODY_COLOR: [[f64; 3]; 242] = [ 179 | [1.00000000, 0.18172716, 0.00000000], // 1000K 180 | [1.00000000, 0.25503671, 0.00000000], // 1100K 181 | [1.00000000, 0.30942099, 0.00000000], // 1200K 182 | [1.00000000, 0.35357379, 0.00000000], 183 | [1.00000000, 0.39091524, 0.00000000], 184 | [1.00000000, 0.42322816, 0.00000000], 185 | [1.00000000, 0.45159884, 0.00000000], 186 | [1.00000000, 0.47675916, 0.00000000], 187 | [1.00000000, 0.49923747, 0.00000000], 188 | [1.00000000, 0.51943421, 0.00000000], 189 | [1.00000000, 0.54360078, 0.08679949], 190 | [1.00000000, 0.56618736, 0.14065513], 191 | [1.00000000, 0.58734976, 0.18362641], 192 | [1.00000000, 0.60724493, 0.22137978], 193 | [1.00000000, 0.62600248, 0.25591950], 194 | [1.00000000, 0.64373109, 0.28819679], 195 | [1.00000000, 0.66052319, 0.31873863], 196 | [1.00000000, 0.67645822, 0.34786758], 197 | [1.00000000, 0.69160518, 0.37579588], 198 | [1.00000000, 0.70602449, 0.40267128], 199 | [1.00000000, 0.71976951, 0.42860152], 200 | [1.00000000, 0.73288760, 0.45366838], 201 | [1.00000000, 0.74542112, 0.47793608], 202 | [1.00000000, 0.75740814, 0.50145662], 203 | [1.00000000, 0.76888303, 0.52427322], 204 | [1.00000000, 0.77987699, 0.54642268], 205 | [1.00000000, 0.79041843, 0.56793692], 206 | [1.00000000, 0.80053332, 0.58884417], 207 | [1.00000000, 0.81024551, 0.60916971], 208 | [1.00000000, 0.81957693, 0.62893653], 209 | [1.00000000, 0.82854786, 0.64816570], 210 | [1.00000000, 0.83717703, 0.66687674], 211 | [1.00000000, 0.84548188, 0.68508786], 212 | [1.00000000, 0.85347859, 0.70281616], 213 | [1.00000000, 0.86118227, 0.72007777], 214 | [1.00000000, 0.86860704, 0.73688797], 215 | [1.00000000, 0.87576611, 0.75326132], 216 | [1.00000000, 0.88267187, 0.76921169], 217 | [1.00000000, 0.88933596, 0.78475236], 218 | [1.00000000, 0.89576933, 0.79989606], 219 | [1.00000000, 0.90198230, 0.81465502], 220 | [1.00000000, 0.90963069, 0.82838210], 221 | [1.00000000, 0.91710889, 0.84190889], 222 | [1.00000000, 0.92441842, 0.85523742], 223 | [1.00000000, 0.93156127, 0.86836903], 224 | [1.00000000, 0.93853986, 0.88130458], 225 | [1.00000000, 0.94535695, 0.89404470], 226 | [1.00000000, 0.95201559, 0.90658983], 227 | [1.00000000, 0.95851906, 0.91894041], 228 | [1.00000000, 0.96487079, 0.93109690], 229 | [1.00000000, 0.97107439, 0.94305985], 230 | [1.00000000, 0.97713351, 0.95482993], 231 | [1.00000000, 0.98305189, 0.96640795], 232 | [1.00000000, 0.98883326, 0.97779486], 233 | [1.00000000, 0.99448139, 0.98899179], 234 | [1.00000000, 1.00000000, 1.00000000], // 6500K 235 | [0.98947904, 0.99348723, 1.00000000], 236 | [0.97940448, 0.98722715, 1.00000000], 237 | [0.96975025, 0.98120637, 1.00000000], 238 | [0.96049223, 0.97541240, 1.00000000], 239 | [0.95160805, 0.96983355, 1.00000000], 240 | [0.94303638, 0.96443333, 1.00000000], 241 | [0.93480451, 0.95923080, 1.00000000], 242 | [0.92689056, 0.95421394, 1.00000000], 243 | [0.91927697, 0.94937330, 1.00000000], 244 | [0.91194747, 0.94470005, 1.00000000], 245 | [0.90488690, 0.94018594, 1.00000000], 246 | [0.89808115, 0.93582323, 1.00000000], 247 | [0.89151710, 0.93160469, 1.00000000], 248 | [0.88518247, 0.92752354, 1.00000000], 249 | [0.87906581, 0.92357340, 1.00000000], 250 | [0.87315640, 0.91974827, 1.00000000], 251 | [0.86744421, 0.91604254, 1.00000000], 252 | [0.86191983, 0.91245088, 1.00000000], 253 | [0.85657444, 0.90896831, 1.00000000], 254 | [0.85139976, 0.90559011, 1.00000000], 255 | [0.84638799, 0.90231183, 1.00000000], 256 | [0.84153180, 0.89912926, 1.00000000], 257 | [0.83682430, 0.89603843, 1.00000000], 258 | [0.83225897, 0.89303558, 1.00000000], 259 | [0.82782969, 0.89011714, 1.00000000], 260 | [0.82353066, 0.88727974, 1.00000000], 261 | [0.81935641, 0.88452017, 1.00000000], 262 | [0.81530175, 0.88183541, 1.00000000], 263 | [0.81136180, 0.87922257, 1.00000000], 264 | [0.80753191, 0.87667891, 1.00000000], 265 | [0.80380769, 0.87420182, 1.00000000], 266 | [0.80018497, 0.87178882, 1.00000000], 267 | [0.79665980, 0.86943756, 1.00000000], 268 | [0.79322843, 0.86714579, 1.00000000], 269 | [0.78988728, 0.86491137, 1.00000000], // 10000K 270 | [0.78663296, 0.86273225, 1.00000000], 271 | [0.78346225, 0.86060650, 1.00000000], 272 | [0.78037207, 0.85853224, 1.00000000], 273 | [0.77735950, 0.85650771, 1.00000000], 274 | [0.77442176, 0.85453121, 1.00000000], 275 | [0.77155617, 0.85260112, 1.00000000], 276 | [0.76876022, 0.85071588, 1.00000000], 277 | [0.76603147, 0.84887402, 1.00000000], 278 | [0.76336762, 0.84707411, 1.00000000], 279 | [0.76076645, 0.84531479, 1.00000000], 280 | [0.75822586, 0.84359476, 1.00000000], 281 | [0.75574383, 0.84191277, 1.00000000], 282 | [0.75331843, 0.84026762, 1.00000000], 283 | [0.75094780, 0.83865816, 1.00000000], 284 | [0.74863017, 0.83708329, 1.00000000], 285 | [0.74636386, 0.83554194, 1.00000000], 286 | [0.74414722, 0.83403311, 1.00000000], 287 | [0.74197871, 0.83255582, 1.00000000], 288 | [0.73985682, 0.83110912, 1.00000000], 289 | [0.73778012, 0.82969211, 1.00000000], 290 | [0.73574723, 0.82830393, 1.00000000], 291 | [0.73375683, 0.82694373, 1.00000000], 292 | [0.73180765, 0.82561071, 1.00000000], 293 | [0.72989845, 0.82430410, 1.00000000], 294 | [0.72802807, 0.82302316, 1.00000000], 295 | [0.72619537, 0.82176715, 1.00000000], 296 | [0.72439927, 0.82053539, 1.00000000], 297 | [0.72263872, 0.81932722, 1.00000000], 298 | [0.72091270, 0.81814197, 1.00000000], 299 | [0.71922025, 0.81697905, 1.00000000], 300 | [0.71756043, 0.81583783, 1.00000000], 301 | [0.71593234, 0.81471775, 1.00000000], 302 | [0.71433510, 0.81361825, 1.00000000], 303 | [0.71276788, 0.81253878, 1.00000000], 304 | [0.71122987, 0.81147883, 1.00000000], 305 | [0.70972029, 0.81043789, 1.00000000], 306 | [0.70823838, 0.80941546, 1.00000000], 307 | [0.70678342, 0.80841109, 1.00000000], 308 | [0.70535469, 0.80742432, 1.00000000], 309 | [0.70395153, 0.80645469, 1.00000000], 310 | [0.70257327, 0.80550180, 1.00000000], 311 | [0.70121928, 0.80456522, 1.00000000], 312 | [0.69988894, 0.80364455, 1.00000000], 313 | [0.69858167, 0.80273941, 1.00000000], 314 | [0.69729688, 0.80184943, 1.00000000], 315 | [0.69603402, 0.80097423, 1.00000000], 316 | [0.69479255, 0.80011347, 1.00000000], 317 | [0.69357196, 0.79926681, 1.00000000], 318 | [0.69237173, 0.79843391, 1.00000000], 319 | [0.69119138, 0.79761446, 1.00000000], // 15000K 320 | [0.69003044, 0.79680814, 1.00000000], 321 | [0.68888844, 0.79601466, 1.00000000], 322 | [0.68776494, 0.79523371, 1.00000000], 323 | [0.68665951, 0.79446502, 1.00000000], 324 | [0.68557173, 0.79370830, 1.00000000], 325 | [0.68450119, 0.79296330, 1.00000000], 326 | [0.68344751, 0.79222975, 1.00000000], 327 | [0.68241029, 0.79150740, 1.00000000], 328 | [0.68138918, 0.79079600, 1.00000000], 329 | [0.68038380, 0.79009531, 1.00000000], 330 | [0.67939381, 0.78940511, 1.00000000], 331 | [0.67841888, 0.78872517, 1.00000000], 332 | [0.67745866, 0.78805526, 1.00000000], 333 | [0.67651284, 0.78739518, 1.00000000], 334 | [0.67558112, 0.78674472, 1.00000000], 335 | [0.67466317, 0.78610368, 1.00000000], 336 | [0.67375872, 0.78547186, 1.00000000], 337 | [0.67286748, 0.78484907, 1.00000000], 338 | [0.67198916, 0.78423512, 1.00000000], 339 | [0.67112350, 0.78362984, 1.00000000], 340 | [0.67027024, 0.78303305, 1.00000000], 341 | [0.66942911, 0.78244457, 1.00000000], 342 | [0.66859988, 0.78186425, 1.00000000], 343 | [0.66778228, 0.78129191, 1.00000000], 344 | [0.66697610, 0.78072740, 1.00000000], 345 | [0.66618110, 0.78017057, 1.00000000], 346 | [0.66539706, 0.77962127, 1.00000000], 347 | [0.66462376, 0.77907934, 1.00000000], 348 | [0.66386098, 0.77854465, 1.00000000], 349 | [0.66310852, 0.77801705, 1.00000000], 350 | [0.66236618, 0.77749642, 1.00000000], 351 | [0.66163375, 0.77698261, 1.00000000], 352 | [0.66091106, 0.77647551, 1.00000000], 353 | [0.66019791, 0.77597498, 1.00000000], 354 | [0.65949412, 0.77548090, 1.00000000], 355 | [0.65879952, 0.77499315, 1.00000000], 356 | [0.65811392, 0.77451161, 1.00000000], 357 | [0.65743716, 0.77403618, 1.00000000], 358 | [0.65676908, 0.77356673, 1.00000000], 359 | [0.65610952, 0.77310316, 1.00000000], 360 | [0.65545831, 0.77264537, 1.00000000], 361 | [0.65481530, 0.77219324, 1.00000000], 362 | [0.65418036, 0.77174669, 1.00000000], 363 | [0.65355332, 0.77130560, 1.00000000], 364 | [0.65293404, 0.77086988, 1.00000000], 365 | [0.65232240, 0.77043944, 1.00000000], 366 | [0.65171824, 0.77001419, 1.00000000], 367 | [0.65112144, 0.76959404, 1.00000000], 368 | [0.65053187, 0.76917889, 1.00000000], 369 | [0.64994941, 0.76876866, 1.00000000], // 20000K 370 | [0.64937392, 0.76836326, 1.00000000], 371 | [0.64880528, 0.76796263, 1.00000000], 372 | [0.64824339, 0.76756666, 1.00000000], 373 | [0.64768812, 0.76717529, 1.00000000], 374 | [0.64713935, 0.76678844, 1.00000000], 375 | [0.64659699, 0.76640603, 1.00000000], 376 | [0.64606092, 0.76602798, 1.00000000], 377 | [0.64553103, 0.76565424, 1.00000000], 378 | [0.64500722, 0.76528472, 1.00000000], 379 | [0.64448939, 0.76491935, 1.00000000], 380 | [0.64397745, 0.76455808, 1.00000000], 381 | [0.64347129, 0.76420082, 1.00000000], 382 | [0.64297081, 0.76384753, 1.00000000], 383 | [0.64247594, 0.76349813, 1.00000000], 384 | [0.64198657, 0.76315256, 1.00000000], 385 | [0.64150261, 0.76281076, 1.00000000], 386 | [0.64102399, 0.76247267, 1.00000000], 387 | [0.64055061, 0.76213824, 1.00000000], 388 | [0.64008239, 0.76180740, 1.00000000], 389 | [0.63961926, 0.76148010, 1.00000000], 390 | [0.63916112, 0.76115628, 1.00000000], 391 | [0.63870790, 0.76083590, 1.00000000], 392 | [0.63825953, 0.76051890, 1.00000000], 393 | [0.63781592, 0.76020522, 1.00000000], 394 | [0.63737701, 0.75989482, 1.00000000], 395 | [0.63694273, 0.75958764, 1.00000000], 396 | [0.63651299, 0.75928365, 1.00000000], 397 | [0.63608774, 0.75898278, 1.00000000], 398 | [0.63566691, 0.75868499, 1.00000000], 399 | [0.63525042, 0.75839025, 1.00000000], 400 | [0.63483822, 0.75809849, 1.00000000], 401 | [0.63443023, 0.75780969, 1.00000000], 402 | [0.63402641, 0.75752379, 1.00000000], 403 | [0.63362667, 0.75724075, 1.00000000], 404 | [0.63323097, 0.75696053, 1.00000000], 405 | [0.63283925, 0.75668310, 1.00000000], 406 | [0.63245144, 0.75640840, 1.00000000], 407 | [0.63206749, 0.75613641, 1.00000000], 408 | [0.63168735, 0.75586707, 1.00000000], 409 | [0.63131096, 0.75560036, 1.00000000], 410 | [0.63093826, 0.75533624, 1.00000000], 411 | [0.63056920, 0.75507467, 1.00000000], 412 | [0.63020374, 0.75481562, 1.00000000], 413 | [0.62984181, 0.75455904, 1.00000000], 414 | [0.62948337, 0.75430491, 1.00000000], 415 | [0.62912838, 0.75405319, 1.00000000], 416 | [0.62877678, 0.75380385, 1.00000000], 417 | [0.62842852, 0.75355685, 1.00000000], 418 | [0.62808356, 0.75331217, 1.00000000], 419 | [0.62774186, 0.75306977, 1.00000000], // 25000K 420 | [0.62740336, 0.75282962, 1.00000000], // 25100K 421 | ]; 422 | -------------------------------------------------------------------------------- /src/calc_solar.rs: -------------------------------------------------------------------------------- 1 | /* calc_solar.rs -- Solar position calculation 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2010 Jon Lund Steffensen 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | /* From Redshift: Ported from javascript code by U.S. Department of Commerce, 22 | National Oceanic & Atmospheric Administration: 23 | http://www.srrb.noaa.gov/highlights/sunrise/calcdetails.html 24 | It is based on equations from "Astronomical Algorithms" by Jean Meeus. 25 | */ 26 | 27 | #![allow(dead_code)] 28 | 29 | use std::f64::consts::PI; 30 | 31 | macro_rules! rad { 32 | ($x:expr) => { 33 | $x * PI / 180.0 34 | }; 35 | } 36 | 37 | // Model of atmospheric refraction near horizon (in degrees) 38 | const SOLAR_ATM_REFRAC: f64 = 0.833; 39 | 40 | const SOLAR_ASTRO_TWILIGHT_ELEV: f64 = -18.0; 41 | const SOLAR_NAUT_TWILIGHT_ELEV: f64 = -12.0; 42 | pub const SOLAR_CIVIL_TWILIGHT_ELEV: f64 = -6.0; 43 | const SOLAR_DAYTIME_ELEV: f64 = 0.0 - SOLAR_ATM_REFRAC; 44 | 45 | // Angels of various times of day 46 | const SOLAR_TIME_MAX: usize = 10; 47 | const TIME_ANGLE: [f64; SOLAR_TIME_MAX] = [ 48 | rad!(0.0), // Noon 49 | rad!(0.0), // Midnight 50 | rad!(-90.0 + SOLAR_ASTRO_TWILIGHT_ELEV), // AstroDawn 51 | rad!(-90.0 + SOLAR_NAUT_TWILIGHT_ELEV), // NautDawn 52 | rad!(-90.0 + SOLAR_CIVIL_TWILIGHT_ELEV), // CivilDawn 53 | rad!(-90.0 + SOLAR_DAYTIME_ELEV), // Sunrise 54 | rad!(90.0 - SOLAR_DAYTIME_ELEV), // Sunset 55 | rad!(90.0 - SOLAR_CIVIL_TWILIGHT_ELEV), // CivilDusk 56 | rad!(90.0 - SOLAR_NAUT_TWILIGHT_ELEV), // NautDusk 57 | rad!(90.0 - SOLAR_ASTRO_TWILIGHT_ELEV), // AstroDusk 58 | ]; 59 | 60 | /// Unix epoch from Julian day 61 | fn epoch_from_jd(jd: f64) -> f64 { 62 | 86400.0 * (jd - 2440587.5) 63 | } 64 | 65 | /// Julian day from unix epoch 66 | fn jd_from_epoch(t: f64) -> f64 { 67 | t / 86400.0 + 2440587.5 68 | } 69 | 70 | /// Julian centuries since J2000.0 from Julian day 71 | fn jcent_from_jd(jd: f64) -> f64 { 72 | (jd - 2451545.0f64) / 36525.0f64 73 | } 74 | 75 | /// Julian day from Julian centuries since J2000.0 76 | fn jd_from_jcent(t: f64) -> f64 { 77 | 36525.0 * t + 2451545.0 78 | } 79 | 80 | /// Geometric mean longitude of the sun 81 | /// t: Julian centuries since J2000.0 82 | /// Return: Geometric mean longitude in radians 83 | fn sun_geom_mean_lon(t: f64) -> f64 { 84 | // FIXME returned value should always be positive 85 | ((280.46646 + t * (36000.76983 + t * 0.0003032)) % 360.0).to_radians() 86 | } 87 | 88 | /// Geometric mean anomaly of the sun 89 | /// t: Julian centuries since J2000.0 90 | /// Return: Geometric mean anomaly in radians 91 | fn sun_geom_mean_anomaly(t: f64) -> f64 { 92 | (357.52911 + t * (35999.05029 - t * 0.0001537)).to_radians() 93 | } 94 | 95 | /// Eccentricity of earth orbit 96 | /// t: Julian centuries since J2000.0 97 | /// Return: Eccentricity (unitless) 98 | fn earth_orbit_eccentricity(t: f64) -> f64 { 99 | 0.016708634 - t * (0.000042037 + t * 0.0000001267) 100 | } 101 | 102 | /// Equation of center of the sun 103 | /// t: Julian centuries since J2000.0 104 | /// Return: Center(?) in radians 105 | fn sun_equation_of_center(t: f64) -> f64 { 106 | let m = sun_geom_mean_anomaly(t); 107 | let c = (m).sin() * (1.914602 - t * (0.004817 + 0.000014 * t)) 108 | + (2.0 * m).sin() * (0.019993 - 0.000101 * t) 109 | + (3.0 * m).sin() * 0.000289; 110 | c.to_radians() 111 | } 112 | 113 | /// True longitude of the sun 114 | /// t: Julian centuries since J2000.0 115 | /// Return: True longitude in radians 116 | fn sun_true_lon(t: f64) -> f64 { 117 | let l_0 = sun_geom_mean_lon(t); 118 | let c = sun_equation_of_center(t); 119 | l_0 + c 120 | } 121 | 122 | /// Apparent longitude of the sun. (Right ascension) 123 | /// t: Julian centuries since J2000.0 124 | /// Return: Apparent longitude in radians 125 | fn sun_apparent_lon(t: f64) -> f64 { 126 | let o = sun_true_lon(t); 127 | (o.to_degrees() 128 | - 0.00569 129 | - 0.00478 * (125.04 - 1934.136 * t).to_radians().sin()) 130 | .to_radians() 131 | } 132 | 133 | /// Mean obliquity of the ecliptic 134 | /// t: Julian centuries since J2000.0 135 | /// Return: Mean obliquity in radians 136 | fn mean_ecliptic_obliquity(t: f64) -> f64 { 137 | let sec = 21.448 - t * (46.815 + t * (0.00059 - t * 0.001813)); 138 | (23.0 + (26.0 + (sec / 60.0)) / 60.0).to_radians() 139 | } 140 | 141 | /// Corrected obliquity of the ecliptic 142 | /// t: Julian centuries since J2000.0 143 | /// Return: Corrected obliquity in radians 144 | fn obliquity_corr(t: f64) -> f64 { 145 | let e_0 = mean_ecliptic_obliquity(t); 146 | let omega = 125.04 - t * 1934.136; 147 | (e_0.to_degrees() + 0.00256 * omega.to_radians().cos()).to_radians() 148 | } 149 | 150 | /// Declination of the sun 151 | /// t: Julian centuries since J2000.0 152 | /// Return: Declination in radians 153 | fn solar_declination(t: f64) -> f64 { 154 | let e = obliquity_corr(t); 155 | let lambda = sun_apparent_lon(t); 156 | ((e).sin() * (lambda)).asin() 157 | } 158 | 159 | /// Difference between true solar time and mean solar time 160 | /// t: Julian centuries since J2000.0 161 | /// Return: Difference in minutes 162 | fn equation_of_time(t: f64) -> f64 { 163 | let epsilon = obliquity_corr(t); 164 | let l_0 = sun_geom_mean_lon(t); 165 | let e = earth_orbit_eccentricity(t); 166 | let m = sun_geom_mean_anomaly(t); 167 | let y = (epsilon / 2.0).tan().powf(2.0); 168 | 169 | let eq_time = y * (2.0 * l_0).sin() - 2.0 * e * m.sin() 170 | + 4.0 * e * y * m.sin() * (2.0 * l_0).cos() 171 | - 0.5 * y * y * (4.0 * l_0).sin() 172 | - 1.25 * e * e * (2.0 * m).sin(); 173 | 4.0 * eq_time.to_degrees() 174 | } 175 | 176 | /// Hour angle at the location for the given angular elevation 177 | /// lat: Latitude of location in degrees 178 | /// decl: Declination in radians 179 | /// elev: Angular elevation angle in radians 180 | /// Return: Hour angle in radians 181 | fn hour_angle_from_elevation(lat: f64, decl: f64, elev: f64) -> f64 { 182 | let omega = elev.abs().cos() - lat.to_radians().sin() * decl.sin(); 183 | let omega = omega / lat.to_radians().cos() * decl.cos(); 184 | omega.acos().copysign(-elev) 185 | } 186 | 187 | /// Angular elevation at the location for the given hour angle 188 | /// lat: Latitude of location in degrees 189 | /// decl: Declination in radians 190 | /// ha: Hour angle in radians 191 | /// Return: Angular elevation in radians 192 | fn elevation_from_hour_angle(lat: f64, decl: f64, ha: f64) -> f64 { 193 | (ha.cos() * lat.to_radians().cos() * decl.cos() 194 | + lat.to_radians().sin() * decl.sin()) 195 | .asin() 196 | } 197 | 198 | /// Time of apparent solar noon of location on earth 199 | /// t: Julian centuries since J2000.0 200 | /// lon: Longitude of location in degrees 201 | /// Return: Time difference from mean solar midnigth in minutes 202 | fn time_of_solar_noon(t: f64, lon: f64) -> f64 { 203 | // First pass uses approximate solar noon to 204 | // calculate equation of time 205 | let mut t_noon = jcent_from_jd(jd_from_jcent(t) - lon / 360.0); 206 | let mut eq_time = equation_of_time(t_noon); 207 | let mut sol_noon = 720.0 - 4.0 * lon - eq_time; 208 | 209 | // Recalculate using new solar noon 210 | t_noon = jcent_from_jd(jd_from_jcent(t) - 0.5 + sol_noon / 1440.0); 211 | eq_time = equation_of_time(t_noon); 212 | sol_noon = 720.0 - 4.0 * lon - eq_time; 213 | // No need to do more iterations 214 | sol_noon 215 | } 216 | 217 | /// Time of given apparent solar angular elevation of location on earth 218 | /// t: Julian centuries since J2000.0 219 | /// t_noon: Apparent solar noon in Julian centuries since J2000.0 220 | /// lat: Latitude of location in degrees 221 | /// lon: Longtitude of location in degrees 222 | /// elev: Solar angular elevation in radians 223 | /// Return: Time difference from mean solar midnight in minutes 224 | fn time_of_solar_elevation( 225 | t: f64, 226 | t_noon: f64, 227 | lat: f64, 228 | lon: f64, 229 | elev: f64, 230 | ) -> f64 { 231 | // First pass uses approximate sunrise to 232 | // calculate equation of time 233 | let mut eq_time = equation_of_time(t_noon); 234 | let mut sol_decl = solar_declination(t_noon); 235 | let mut ha = hour_angle_from_elevation(lat, sol_decl, elev); 236 | let mut sol_offset = 720.0 - 4.0 * (lon + ha.to_degrees()) - eq_time; 237 | 238 | // Recalculate using new sunrise 239 | let t_rise = jcent_from_jd(jd_from_jcent(t) + sol_offset / 1440.0); 240 | eq_time = equation_of_time(t_rise); 241 | sol_decl = solar_declination(t_rise); 242 | ha = hour_angle_from_elevation(lat, sol_decl, elev); 243 | sol_offset = 720.0 - 4.0 * (lon + ha.to_degrees()) - eq_time; 244 | // No need to do more iterations 245 | sol_offset 246 | } 247 | 248 | /// Solar angular elevation at the given location and time 249 | /// t: Julian centuries since J2000.0 250 | /// lat: Latitude of location 251 | /// lon: Longitude of location 252 | /// Return: Solar angular elevation in radians 253 | fn solar_elevation_from_time(t: f64, lat: f64, lon: f64) -> f64 { 254 | // Minutes from midnight 255 | let jd = jd_from_jcent(t); 256 | let offset = (jd - jd.round() - 0.5) * 1440.0; 257 | let eq_time = equation_of_time(t); 258 | let ha = ((720.0 - offset - eq_time) / 4.0 - lon).to_radians(); 259 | let decl = solar_declination(t); 260 | elevation_from_hour_angle(lat, decl, ha) 261 | } 262 | 263 | /// Solar angular elevation at the given location and time 264 | /// date: Seconds since unix epoch 265 | /// lat: Latitude of location 266 | /// lon: Longitude of location 267 | /// Return: Solar angular elevation in degrees 268 | pub fn solar_elevation(date: f64, lat: f64, lon: f64) -> f64 { 269 | let jd = jd_from_epoch(date); 270 | let jcent = jcent_from_jd(jd); 271 | solar_elevation_from_time(jcent, lat, lon).to_degrees() 272 | } 273 | 274 | fn solar_table_fill( 275 | date: f64, 276 | lat: f64, 277 | lon: f64, 278 | table: &mut [f64; SOLAR_TIME_MAX], 279 | ) { 280 | // Calculate Julian day 281 | let jd = jd_from_epoch(date); 282 | 283 | // Calculate Julian day number 284 | let jdn = jd.round(); 285 | let t = jcent_from_jd(jdn); 286 | 287 | // Calculate apparent solar noon 288 | let sol_noon = time_of_solar_noon(t, lon); 289 | let j_noon = jdn - 0.5 + sol_noon / 1440.0; 290 | let t_noon = jcent_from_jd(j_noon); 291 | table[0] = epoch_from_jd(j_noon); 292 | 293 | // Calculate solar midnight 294 | table[1] = epoch_from_jd(j_noon + 0.5); 295 | 296 | // Calculate absolute time of other phenomena 297 | for i in 2..SOLAR_TIME_MAX { 298 | let angle = TIME_ANGLE[i]; 299 | let offset = time_of_solar_elevation(t, t_noon, lat, lon, angle); 300 | table[i] = epoch_from_jd(jdn - 0.5 + offset / 1440.0); 301 | } 302 | } 303 | 304 | #[cfg(test)] 305 | mod test { 306 | use super::solar_elevation; 307 | use anyhow::Result; 308 | use insta::assert_snapshot; 309 | use std::{fmt::Write, time::Duration}; 310 | 311 | #[test] 312 | fn test_calculating_solar_elevation_degrees_for_null_island() -> Result<()> 313 | { 314 | let res = (0..24).try_fold(String::new(), |mut buff, i| { 315 | let s = Duration::from_secs(i * 3600).as_secs_f64(); 316 | let e = solar_elevation(s, 0.0, 0.0); 317 | writeln!(&mut buff, "1970-01-01 {i:02}:00 {e:6.2}°")?; 318 | Ok::<_, anyhow::Error>(buff) 319 | })?; 320 | 321 | // Results from: https://gml.noaa.gov/grad/antuv/SolarCalc.jsp 322 | // 1970-01-01 00:00 -66.93° 323 | // 1970-01-01 01:00 -63.14° 324 | // 1970-01-01 02:00 -53.46° 325 | // 1970-01-01 03:00 -41.30° 326 | // 1970-01-01 04:00 -28.14° 327 | // 1970-01-01 05:00 -14.54° 328 | // 1970-01-01 06:00 -0.77° 329 | // 1970-01-01 07:00 13.00° 330 | // 1970-01-01 08:00 26.62° 331 | // 1970-01-01 09:00 39.86° 332 | // 1970-01-01 10:00 52.18° 333 | // 1970-01-01 11:00 62.28° 334 | // 1970-01-01 12:00 66.96° 335 | // 1970-01-01 13:00 63.20° 336 | // 1970-01-01 14:00 53.52° 337 | // 1970-01-01 15:00 41.36° 338 | // 1970-01-01 16:00 28.20° 339 | // 1970-01-01 17:00 14.60° 340 | // 1970-01-01 18:00 0.83° 341 | // 1970-01-01 19:00 -12.95° 342 | // 1970-01-01 20:00 -26.58° 343 | // 1970-01-01 21:00 -39.82° 344 | // 1970-01-01 22:00 -52.16° 345 | // 1970-01-01 23:00 -62.28° 346 | 347 | Ok(assert_snapshot!(res, @r###" 348 | 1970-01-01 00:00 -56.32° 349 | 1970-01-01 01:00 -53.81° 350 | 1970-01-01 02:00 -46.63° 351 | 1970-01-01 03:00 -36.68° 352 | 1970-01-01 04:00 -25.28° 353 | 1970-01-01 05:00 -13.15° 354 | 1970-01-01 06:00 -0.71° 355 | 1970-01-01 07:00 11.76° 356 | 1970-01-01 08:00 23.96° 357 | 1970-01-01 09:00 35.51° 358 | 1970-01-01 10:00 45.73° 359 | 1970-01-01 11:00 53.37° 360 | 1970-01-01 12:00 56.56° 361 | 1970-01-01 13:00 54.05° 362 | 1970-01-01 14:00 46.84° 363 | 1970-01-01 15:00 36.84° 364 | 1970-01-01 16:00 25.40° 365 | 1970-01-01 17:00 13.23° 366 | 1970-01-01 18:00 0.76° 367 | 1970-01-01 19:00 -11.74° 368 | 1970-01-01 20:00 -23.98° 369 | 1970-01-01 21:00 -35.58° 370 | 1970-01-01 22:00 -45.86° 371 | 1970-01-01 23:00 -53.57° 372 | "###)) 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | /* config.rs -- Command line interface 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | use crate::{ 20 | config::{DEFAULT_SLEEP_DURATION, DEFAULT_SLEEP_DURATION_SHORT}, 21 | types::{ 22 | AdjustmentMethodType, Brightness, BrightnessRange, Gamma, GammaRange, 23 | LocationProviderType, Temperature, TemperatureRange, TransitionScheme, 24 | MAX_TEMPERATURE, MIN_TEMPERATURE, 25 | }, 26 | }; 27 | use anstream::ColorChoice; 28 | use clap::{ 29 | ArgAction, Args, ColorChoice as ClapColorChoice, Command, CommandFactory, 30 | Parser, Subcommand, 31 | }; 32 | use const_format::formatcp; 33 | use std::{cmp::Ordering, marker::PhantomData, path::PathBuf, str::FromStr}; 34 | use tracing::{level_filters::LevelFilter, Level}; 35 | 36 | const VERSION: &str = { 37 | const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); 38 | const GIT_DESCRIBE: &str = env!("VERGEN_GIT_DESCRIBE"); 39 | const GIT_COMMIT_DATE: &str = env!("VERGEN_GIT_COMMIT_DATE"); 40 | 41 | #[allow(clippy::const_is_empty)] 42 | if GIT_DESCRIBE.is_empty() { 43 | formatcp!("{PKG_VERSION}") 44 | } else { 45 | formatcp!("{PKG_VERSION} ({GIT_DESCRIBE} {GIT_COMMIT_DATE})") 46 | } 47 | }; 48 | 49 | const LONG_VERSION: &str = { 50 | const RUSTC_SEMVER: &str = env!("VERGEN_RUSTC_SEMVER"); 51 | const RUSTC_HOST_TRIPLE: &str = env!("VERGEN_RUSTC_HOST_TRIPLE"); 52 | const CARGO_FEATURES: &str = env!("VERGEN_CARGO_FEATURES"); 53 | const CARGO_TARGET_TRIPLE: &str = env!("VERGEN_CARGO_TARGET_TRIPLE"); 54 | 55 | formatcp!( 56 | "{VERSION} 57 | 58 | rustc version: {RUSTC_SEMVER} 59 | rustc host triple: {RUSTC_HOST_TRIPLE} 60 | cargo features: {CARGO_FEATURES} 61 | cargo target triple: {CARGO_TARGET_TRIPLE}" 62 | ) 63 | }; 64 | 65 | #[derive(Debug, Parser)] 66 | #[command(about, version = VERSION, long_version = LONG_VERSION)] 67 | #[command(propagate_version = true, next_line_help(false))] 68 | pub struct CliArgs { 69 | #[command(subcommand)] 70 | pub mode: ModeArgs, 71 | 72 | /// When to use color: auto, always, never [default: auto] 73 | #[arg(long, value_name = "WHEN", value_parser = ClapColorChoice::from_str)] 74 | #[arg(global = true, display_order(100))] 75 | pub color: Option, 76 | 77 | #[command(flatten)] 78 | pub verbosity: Verbosity, 79 | } 80 | 81 | #[derive(Debug, Subcommand)] 82 | pub enum ModeArgs { 83 | /// Apply screen color settings according to time of day continuously 84 | #[command(next_line_help(true))] 85 | Daemon { 86 | #[command(flatten)] 87 | c: CmdArgs, 88 | 89 | /// Disable fading between color temperatures 90 | /// 91 | /// It will cause an immediate change between screen temperatures. by default, 92 | /// the new screen temperature are gradually applied over a couple of seconds 93 | #[arg(verbatim_doc_comment)] 94 | #[arg(long, action = ArgAction::SetTrue)] 95 | disable_fade: Option, 96 | 97 | #[arg(help = formatcp!("Duration of sleep between screen updates [default: {DEFAULT_SLEEP_DURATION}]"))] 98 | #[arg(long, value_name = "MILLISECONDS")] 99 | sleep_duration: Option, 100 | 101 | #[arg(help = formatcp!("Duration of sleep between screen updates for fade [default: {DEFAULT_SLEEP_DURATION_SHORT}]"))] 102 | #[arg(long, value_name = "MILLISECONDS")] 103 | sleep_duration_short: Option, 104 | }, 105 | 106 | /// Like daemon mode, but do not run continuously 107 | #[command(next_line_help(true))] 108 | Oneshot { 109 | #[command(flatten)] 110 | c: CmdArgs, 111 | }, 112 | 113 | /// Apply a specific screen color settings 114 | #[command(next_line_help(true))] 115 | Set { 116 | #[command(flatten)] 117 | cs: ColorSettingsArgs, 118 | #[command(flatten)] 119 | i: CmdInnerArgs, 120 | }, 121 | 122 | /// Remove adjustment from screen 123 | #[command(next_line_help(true))] 124 | Reset { 125 | #[command(flatten)] 126 | i: CmdInnerArgs, 127 | }, 128 | 129 | /// Print all solar elevation angles for the next 24 hours 130 | #[command(next_line_help(true))] 131 | Print { 132 | /// Location [default: 0:0] 133 | /// 134 | /// Either set latitude and longitude manually or select a location provider. 135 | /// Negative values represent west and south, respectively. e.g.: 136 | /// 51.48:0.0 (Greenwich) 137 | /// geoclue2 (Currently not available) 138 | #[arg(verbatim_doc_comment)] 139 | #[arg(long, short, value_parser = LocationProviderType::from_str)] 140 | #[arg(value_name = "LATITUDE:LONGITUDE | PROVIDER")] 141 | #[arg(allow_hyphen_values = true)] 142 | location: LocationProviderType, 143 | }, 144 | } 145 | 146 | #[derive(Debug, Args)] 147 | pub struct ColorSettingsArgs { 148 | /// Color temperature to apply [default: 6500] 149 | /// 150 | /// The neutral temperature is 6500K. Using this value will not change the color 151 | /// temperature of the display. Setting the color temperature to a value higher 152 | /// than this results in more blue light, and setting a lower value will result 153 | /// in more red light. 154 | #[arg(verbatim_doc_comment)] 155 | #[arg(long, short, value_parser = Temperature::from_str)] 156 | #[arg(value_name = formatcp!("FROM {MIN_TEMPERATURE} TO {MAX_TEMPERATURE}"))] 157 | #[arg(default_value_t)] 158 | pub temperature: Temperature, 159 | 160 | /// Additional gamma correction to apply [default: 1.0] 161 | /// 162 | /// Either set it for all colors, or each color channel individually. e.g.: 163 | /// 0.9 (R=G=B=0.9) 164 | /// 0.8:0.9:0.9 (R=0.8, G=0.9, B=0.9) 165 | #[arg(verbatim_doc_comment)] 166 | #[arg(long, short, value_parser = Gamma::from_str)] 167 | #[arg(value_name = "FROM 0.1 TO 10")] 168 | #[arg(default_value_t)] 169 | pub gamma: Gamma, 170 | 171 | /// Screen brightness to apply [default: 1.0] 172 | #[arg(verbatim_doc_comment)] 173 | #[arg(long, short, value_parser = Brightness::from_str)] 174 | #[arg(value_name = "FROM 0.1 TO 1.0")] 175 | #[arg(default_value_t)] 176 | pub brightness: Brightness, 177 | } 178 | 179 | #[derive(Debug, Args)] 180 | pub struct CmdArgs { 181 | /// Color temperature to set for day and night [default: 6500-4500] 182 | /// 183 | /// The neutral temperature is 6500K. Using this value will not change the color 184 | /// temperature of the display. Setting the color temperature to a value higher 185 | /// than this results in more blue light, and setting a lower value will result 186 | /// in more red light. e.g.: 187 | /// 5000 (day=night=5000) 188 | /// 6500-4500 (day=6500, night=4500) 189 | #[arg(verbatim_doc_comment)] 190 | #[arg(long, short, value_parser = TemperatureRange::from_str)] 191 | #[arg(value_name = formatcp!("FROM {MIN_TEMPERATURE} TO {MAX_TEMPERATURE}"))] 192 | pub temperature: Option, 193 | 194 | /// Additional gamma correction to apply for day and night [default: 1.0] 195 | /// 196 | /// Either set it for all colors, or each color channel individually. e.g.: 197 | /// 0.9 (day=night=0.9) 198 | /// 1.0 - 0.8:0.9:0.9 (day=1.0, night=(R=0.8, G=0.9, B=0.9)) 199 | #[arg(verbatim_doc_comment)] 200 | #[arg(long, short, value_parser = GammaRange::from_str)] 201 | #[arg(value_name = "FROM 0.1 TO 10")] 202 | pub gamma: Option, 203 | 204 | /// Screen brightness to apply for day and night [default: 1.0] 205 | /// 206 | /// It is a fake brightness adjustment obtained by manipulating the gamma ramps 207 | /// which means that it does not reduce the backlight of the screen. e.g.: 208 | /// 0.8 (day=night=0.8) 209 | /// 1.0-0.8 (day=1.0, night=0.8) 210 | #[arg(verbatim_doc_comment)] 211 | #[arg(long, short, value_parser = BrightnessRange::from_str)] 212 | #[arg(value_name = "FROM 0.1 TO 1.0")] 213 | pub brightness: Option, 214 | 215 | /// Transition scheme [default: 3:-6] 216 | /// 217 | /// Either time ranges or elevation angles. By default, Reddish Shift will use 218 | /// the current elevation of the sun to determine whether it is daytime, night 219 | /// or in transition (dawn/dusk). You can also use the print command to see 220 | /// solar elevation angles for the next 24 hours. e.g.: 221 | /// 6:00-7:45 - 18:35-20:15 (dawn=6:00-7:45, dusk=18:35-20:15) 222 | /// 7:45 - 18:35 (day starts at 7:45, night starts at 20:15) 223 | /// 3:-6 (above 3° is day, bellow -6° is night) 224 | #[arg(verbatim_doc_comment)] 225 | #[arg(long, short, value_parser = TransitionScheme::from_str)] 226 | #[arg(value_name = "TIME-TIME - TIME-TIME | TIME-TIME | DEGREE:DEGREE")] 227 | #[arg(allow_hyphen_values = true)] 228 | pub scheme: Option, 229 | 230 | /// Location, used for computation of current solar elevation [default: 0:0] 231 | /// 232 | /// It is not needed when using manual time ranges for transition scheme Either 233 | /// set latitude and longitude manually or select a location provider. Negative 234 | /// values represent west and south, respectively. e.g.: 235 | /// 51.48:0.0 (Greenwich) 236 | /// geoclue2 (Currently not available) 237 | #[arg(verbatim_doc_comment)] 238 | #[arg(long, short, value_parser = LocationProviderType::from_str)] 239 | #[arg(value_name = "LATITUDE:LONGITUDE | PROVIDER")] 240 | #[arg(allow_hyphen_values = true)] 241 | pub location: Option, 242 | 243 | #[command(flatten)] 244 | pub i: CmdInnerArgs, 245 | } 246 | 247 | #[derive(Debug, Args)] 248 | pub struct CmdInnerArgs { 249 | /// Adjustment method to use to apply color settings 250 | /// 251 | /// If not set, the first available method will be used. e.g.: 252 | /// dummy (does not affect the display) 253 | /// XVidMode extension: 254 | /// vidmode (apply to $DISPLAY) 255 | /// vidmode:0 (apply to screen 0) 256 | /// XRANDR extension: 257 | /// randr (apply to $DISPLAY) 258 | /// randr:0 (apply to screen 0) 259 | /// randr$DISPLAY:62,63 (apply to $DISPLAY with crtcs 62 and 63) 260 | /// Direct Rendering Manager: 261 | /// drm (apply to /dev/dri/card0) 262 | /// drm:1 (apply to /dev/dri/card1) 263 | /// drm:0:80 (apply to /dev/dri/card0 with crtc 80) 264 | /// Windows graphics device interface: 265 | /// win32gdi (apply to current display) 266 | #[arg(verbatim_doc_comment)] 267 | #[arg(long, short, value_parser = AdjustmentMethodType::from_str)] 268 | #[arg( 269 | value_name = "METHOD [:(DISPLAY_NUM | CARD_NUM) [:CRTC1,CRTC2,...]]" 270 | )] 271 | pub method: Option, 272 | 273 | /// Reset existing gamma ramps before applying new color settings 274 | #[arg(long, action = ArgAction::SetTrue)] 275 | pub reset_ramps: Option, 276 | 277 | /// Path of the config file 278 | /// 279 | /// A template for the config file should have been installed alongside 280 | /// the program. 281 | #[arg(long, short, value_name = "FILE", display_order(99))] 282 | pub config: Option, 283 | } 284 | 285 | // 286 | 287 | pub trait DefaultLevel { 288 | fn default() -> Option; 289 | } 290 | 291 | #[derive(Debug, Clone, Copy, Default)] 292 | pub struct InfoLevel; 293 | impl DefaultLevel for InfoLevel { 294 | fn default() -> Option { 295 | Some(Level::INFO) 296 | } 297 | } 298 | 299 | #[derive(Args, Debug, Clone, Copy, Default)] 300 | pub struct Verbosity { 301 | /// Increase verbosity 302 | // #[arg(short, long, action = clap::ArgAction::Count)] 303 | // #[arg(global = true, display_order(100), conflicts_with = "quite")] 304 | #[arg(skip)] 305 | verbose: u8, 306 | 307 | /// Decrease verbosity 308 | #[arg(short, long, action = clap::ArgAction::Count, global = true)] 309 | #[arg(global = true, display_order(100))] 310 | quiet: u8, 311 | 312 | #[arg(skip)] 313 | phantom: PhantomData, 314 | } 315 | 316 | impl Verbosity { 317 | pub fn level_filter(&self) -> LevelFilter { 318 | self.level().into() 319 | } 320 | 321 | /// [None] means all output is disabled. 322 | pub fn level(&self) -> Option { 323 | match self.verbosity() { 324 | i8::MIN..=-1 => None, 325 | 0 => Some(Level::ERROR), 326 | 1 => Some(Level::WARN), 327 | 2 => Some(Level::INFO), 328 | 3 => Some(Level::DEBUG), 329 | 4..=i8::MAX => Some(Level::TRACE), 330 | } 331 | } 332 | 333 | fn verbosity(&self) -> i8 { 334 | Self::level_i8(L::default()) - (self.quiet as i8) 335 | + (self.verbose as i8) 336 | } 337 | 338 | fn level_i8(level: Option) -> i8 { 339 | match level { 340 | None => -1, 341 | Some(Level::ERROR) => 0, 342 | Some(Level::WARN) => 1, 343 | Some(Level::INFO) => 2, 344 | Some(Level::DEBUG) => 3, 345 | Some(Level::TRACE) => 4, 346 | } 347 | } 348 | } 349 | 350 | impl Eq for Verbosity {} 351 | impl PartialEq for Verbosity { 352 | fn eq(&self, other: &Self) -> bool { 353 | self.level() == other.level() 354 | } 355 | } 356 | 357 | impl Ord for Verbosity { 358 | fn cmp(&self, other: &Self) -> Ordering { 359 | self.level().cmp(&other.level()) 360 | } 361 | } 362 | impl PartialOrd for Verbosity { 363 | fn partial_cmp(&self, other: &Self) -> Option { 364 | Some(self.cmp(other)) 365 | } 366 | } 367 | 368 | pub trait ClapColorChoiceExt { 369 | fn to_choice(&self) -> ColorChoice; 370 | } 371 | 372 | impl ClapColorChoiceExt for ClapColorChoice { 373 | fn to_choice(&self) -> ColorChoice { 374 | match self { 375 | ClapColorChoice::Auto => ColorChoice::Auto, 376 | ClapColorChoice::Always => ColorChoice::Always, 377 | ClapColorChoice::Never => ColorChoice::Never, 378 | } 379 | } 380 | } 381 | 382 | // used for generation of shell completion scripts and man pages 383 | 384 | pub fn cli_args_command() -> Command { 385 | CliArgs::command() 386 | } 387 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | /* config.rs -- Hierarchical configuration 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | #[cfg(windows)] 20 | use crate::gamma_win32gdi::Win32Gdi; 21 | #[cfg(unix_without_macos)] 22 | use crate::{gamma_drm::Drm, gamma_randr::Randr, gamma_vidmode::Vidmode}; 23 | 24 | use crate::{ 25 | cli::{ 26 | CliArgs, CmdArgs, CmdInnerArgs, ColorSettingsArgs, ModeArgs, Verbosity, 27 | }, 28 | error::{ 29 | config::{ConfigError, ConfigFileError}, 30 | parse::DayNightErrorType, 31 | VecError, 32 | }, 33 | types::{ 34 | AdjustmentMethodType, BrightnessRange, ColorSettings, DayNight, 35 | GammaRange, LocationProviderType, Mode, TemperatureRange, 36 | TransitionScheme, 37 | }, 38 | types_display::WARN, 39 | utils::IsDefault, 40 | AdjustmentMethod, LocationProvider, Manual, 41 | }; 42 | use chrono::{DateTime, Local}; 43 | use clap::ColorChoice; 44 | use clap::Parser; 45 | #[cfg(unix)] 46 | use const_format::formatcp; 47 | use serde::{de, Deserialize, Deserializer}; 48 | use std::{ 49 | fmt::Display, fs::File, io::Read, marker::PhantomData, path::Path, 50 | str::FromStr, time::Duration, 51 | }; 52 | use toml::Value; 53 | use tracing::warn; 54 | 55 | pub const PKG_NAME: &str = env!("CARGO_PKG_NAME"); 56 | // Length of fade in numbers of fade's sleep durations 57 | pub const FADE_STEPS: u8 = 40; 58 | // Duration of sleep between screen updates (milliseconds) 59 | pub const DEFAULT_SLEEP_DURATION: u64 = 5000; 60 | pub const DEFAULT_SLEEP_DURATION_SHORT: u64 = 100; 61 | 62 | #[cfg(unix_without_macos)] 63 | pub const RANDR_MINOR_VERSION_MIN: u32 = 3; 64 | #[cfg(unix_without_macos)] 65 | pub const RANDR_MAJOR_VERSION: u32 = 1; 66 | 67 | /// Merge of cli arguments and config files from highest priority to lowest: 68 | /// 1. CLI arguments 69 | /// 2. User config file 70 | /// 3. System config file (Unix-like OS's only) 71 | /// 4. Default values 72 | #[derive(Debug)] 73 | pub struct Config { 74 | pub mode: Mode, 75 | 76 | pub day: ColorSettings, 77 | pub night: ColorSettings, 78 | pub reset_ramps: bool, 79 | pub scheme: TransitionScheme, 80 | pub disable_fade: bool, 81 | pub sleep_duration: Duration, 82 | pub sleep_duration_short: Duration, 83 | 84 | pub location: LocationProvider, 85 | pub method: AdjustmentMethod, 86 | pub time: fn() -> DateTime, 87 | } 88 | 89 | #[derive(Debug, Clone, PartialEq)] 90 | pub struct ConfigBuilder { 91 | mode: Mode, 92 | 93 | day: ColorSettings, 94 | night: ColorSettings, 95 | reset_ramps: bool, 96 | disable_fade: bool, 97 | scheme: TransitionScheme, 98 | sleep_duration: Duration, 99 | sleep_duration_short: Duration, 100 | 101 | location: LocationProviderType, 102 | method: Option, 103 | } 104 | 105 | #[derive(Debug, Default, Deserialize)] 106 | #[serde(rename_all = "kebab-case")] 107 | struct ConfigFile { 108 | temperature: Option>, 109 | gamma: Option>, 110 | brightness: Option>, 111 | scheme: Option, 112 | location: Option, 113 | method: Option, 114 | reset_ramps: Option, 115 | disable_fade: Option, 116 | sleep_duration_short: Option, 117 | sleep_duration: Option, 118 | } 119 | 120 | #[derive(Debug, Clone, Default)] 121 | struct Either, T> { 122 | t: T, 123 | p: PhantomData, 124 | } 125 | 126 | impl ConfigBuilder { 127 | pub fn new( 128 | logging_init: impl FnOnce(Verbosity, ColorChoice), 129 | ) -> Result { 130 | let cli_args = CliArgs::parse(); 131 | logging_init(cli_args.verbosity, cli_args.color.unwrap_or_default()); 132 | 133 | let mut cfg = Self::default(); 134 | if let Some(path) = Self::config_path_from_mode(&cli_args.mode) { 135 | let config_file = ConfigFile::new(path)?; 136 | cfg.merge_with_config_file(config_file); 137 | } 138 | cfg.merge_with_cli_args(cli_args); 139 | 140 | Ok(cfg) 141 | } 142 | 143 | pub fn build(self) -> Result { 144 | let Self { 145 | mode, 146 | day, 147 | night, 148 | reset_ramps, 149 | disable_fade, 150 | scheme, 151 | sleep_duration, 152 | sleep_duration_short, 153 | location, 154 | method, 155 | } = self; 156 | 157 | Ok(Config { 158 | location: Self::get_location_provider(location, mode, &scheme), 159 | method: Self::get_adjustment_method(method, mode)?, 160 | time: Local::now, 161 | mode, 162 | day, 163 | night, 164 | reset_ramps, 165 | scheme, 166 | disable_fade, 167 | sleep_duration_short, 168 | sleep_duration, 169 | }) 170 | } 171 | 172 | fn get_location_provider( 173 | kind: LocationProviderType, 174 | mode: Mode, 175 | scheme: &TransitionScheme, 176 | ) -> LocationProvider { 177 | match kind { 178 | LocationProviderType::Manual(l) => { 179 | if let ( 180 | Mode::Daemon | Mode::Oneshot, 181 | TransitionScheme::Elev(_), 182 | true, 183 | ) = (mode, scheme, l.is_default()) 184 | { 185 | warn!( 186 | "{WARN}warning:{WARN:#} using default location ({l})" 187 | ); 188 | } 189 | LocationProvider::Manual(Manual::new(l)) 190 | } 191 | LocationProviderType::Geoclue2 => { 192 | LocationProvider::Geoclue2(Default::default()) 193 | } 194 | } 195 | } 196 | 197 | #[allow(clippy::too_many_lines)] 198 | fn get_adjustment_method( 199 | kind: Option, 200 | mode: Mode, 201 | ) -> Result { 202 | match (mode, kind) { 203 | (Mode::Print, _) => { 204 | Ok(AdjustmentMethod::Dummy(Default::default())) 205 | } 206 | 207 | (_, Some(m)) => match m { 208 | AdjustmentMethodType::Dummy => { 209 | let s = "using dummy method! display will not be affected"; 210 | warn!("{WARN}warning:{WARN:#} {s}"); 211 | Ok(AdjustmentMethod::Dummy(Default::default())) 212 | } 213 | #[cfg(unix_without_macos)] 214 | AdjustmentMethodType::Drm { card_num, crtcs } => { 215 | Ok(AdjustmentMethod::Drm(Drm::new(card_num, crtcs)?)) 216 | } 217 | #[cfg(unix_without_macos)] 218 | AdjustmentMethodType::Randr { screen_num, crtcs } => { 219 | Ok(AdjustmentMethod::Randr(Randr::new(screen_num, crtcs)?)) 220 | } 221 | #[cfg(unix_without_macos)] 222 | AdjustmentMethodType::Vidmode { screen_num } => { 223 | Ok(AdjustmentMethod::Vidmode(Vidmode::new(screen_num)?)) 224 | } 225 | 226 | #[cfg(windows)] 227 | AdjustmentMethodType::Win32Gdi => { 228 | Ok(AdjustmentMethod::Win32Gdi(Win32Gdi::new()?)) 229 | } 230 | }, 231 | 232 | (_, None) => { 233 | let s = "trying all methods until one that works is found"; 234 | warn!("{WARN}warning:{WARN:#} {s}"); 235 | let r = Err::(VecError::default()); 236 | 237 | #[cfg(unix_without_macos)] 238 | let r = r 239 | .or_else(|errs| -> Result<_, VecError<_>> { 240 | let m = Randr::new(None, Vec::new()) 241 | .map_err(|e| errs.push(e.into()))?; 242 | Ok(AdjustmentMethod::Randr(m)) 243 | }) 244 | .or_else(|errs| -> Result<_, VecError<_>> { 245 | let m = Vidmode::new(None) 246 | .map_err(|e| errs.push(e.into()))?; 247 | Ok(AdjustmentMethod::Vidmode(m)) 248 | }) 249 | .or_else(|errs| -> Result<_, VecError<_>> { 250 | let m = Drm::new(None, Vec::new()) 251 | .map_err(|e| errs.push(e.into()))?; 252 | Ok(AdjustmentMethod::Drm(m)) 253 | }); 254 | 255 | #[cfg(windows)] 256 | let r = r.or_else(|errs| -> Result<_, VecError<_>> { 257 | let m = 258 | Win32Gdi::new().map_err(|e| errs.push(e.into()))?; 259 | Ok(AdjustmentMethod::Win32Gdi(m)) 260 | }); 261 | 262 | r.map_err(ConfigError::NoAvailableMethod) 263 | } 264 | } 265 | } 266 | 267 | fn config_path_from_mode(mode: &ModeArgs) -> Option> { 268 | match mode { 269 | ModeArgs::Print { .. } => None, 270 | ModeArgs::Daemon { 271 | c: 272 | CmdArgs { 273 | i: CmdInnerArgs { config, .. }, 274 | .. 275 | }, 276 | .. 277 | } 278 | | ModeArgs::Oneshot { 279 | c: 280 | CmdArgs { 281 | i: CmdInnerArgs { config, .. }, 282 | .. 283 | }, 284 | } 285 | | ModeArgs::Set { 286 | i: CmdInnerArgs { config, .. }, 287 | .. 288 | } 289 | | ModeArgs::Reset { 290 | i: CmdInnerArgs { config, .. }, 291 | } => Some(config.as_deref()), 292 | } 293 | } 294 | 295 | #[allow(clippy::too_many_lines)] 296 | fn merge_with_cli_args(&mut self, cli_args: CliArgs) { 297 | let CliArgs { 298 | mode, 299 | verbosity: _, 300 | color: _, 301 | } = cli_args; 302 | 303 | match mode { 304 | ModeArgs::Daemon { 305 | c, 306 | disable_fade, 307 | sleep_duration, 308 | sleep_duration_short, 309 | } => { 310 | if let Some(t) = sleep_duration { 311 | self.sleep_duration = Duration::from_millis(t as u64); 312 | } 313 | if let Some(t) = sleep_duration_short { 314 | self.sleep_duration_short = 315 | Duration::from_millis(t as u64); 316 | } 317 | if let Some(t) = disable_fade { 318 | self.disable_fade = t; 319 | } 320 | self.merge_with_cmd_args(c); 321 | self.mode = Mode::Daemon; 322 | } 323 | ModeArgs::Oneshot { c } => { 324 | self.merge_with_cmd_args(c); 325 | self.mode = Mode::Oneshot; 326 | } 327 | ModeArgs::Set { cs, i } => { 328 | self.merge_with_inner_cmd_args(i); 329 | self.day = cs.into(); 330 | self.mode = Mode::Set; 331 | } 332 | ModeArgs::Reset { i } => { 333 | self.merge_with_inner_cmd_args(i); 334 | self.mode = Mode::Reset; 335 | } 336 | ModeArgs::Print { location } => { 337 | self.location = location; 338 | self.mode = Mode::Print; 339 | } 340 | } 341 | } 342 | 343 | fn merge_with_cmd_args(&mut self, args: CmdArgs) { 344 | let CmdArgs { 345 | temperature, 346 | brightness, 347 | gamma, 348 | scheme, 349 | location, 350 | i, 351 | } = args; 352 | 353 | if let Some(t) = temperature { 354 | self.day.temp = t.day; 355 | self.night.temp = t.night; 356 | } 357 | if let Some(t) = brightness { 358 | self.day.brght = t.day; 359 | self.night.brght = t.night; 360 | } 361 | if let Some(t) = gamma { 362 | self.day.gamma = t.day; 363 | self.night.gamma = t.night; 364 | } 365 | 366 | if let Some(t) = scheme { 367 | self.scheme = t; 368 | } 369 | if let Some(t) = location { 370 | self.location = t; 371 | } 372 | self.merge_with_inner_cmd_args(i); 373 | } 374 | 375 | fn merge_with_inner_cmd_args(&mut self, args: CmdInnerArgs) { 376 | let CmdInnerArgs { 377 | config: _, 378 | reset_ramps, 379 | method, 380 | } = args; 381 | 382 | if let Some(t) = reset_ramps { 383 | self.reset_ramps = t; 384 | } 385 | if let Some(t) = method { 386 | self.method = Some(t); 387 | } 388 | } 389 | 390 | #[allow(clippy::too_many_lines)] 391 | fn merge_with_config_file(&mut self, config: ConfigFile) { 392 | let ConfigFile { 393 | temperature, 394 | brightness, 395 | gamma, 396 | reset_ramps, 397 | scheme, 398 | disable_fade, 399 | sleep_duration_short, 400 | sleep_duration, 401 | method, 402 | location, 403 | } = config; 404 | 405 | if let Some(t) = temperature { 406 | self.day.temp = t.t.day; 407 | self.night.temp = t.t.night; 408 | } 409 | if let Some(t) = brightness { 410 | self.day.brght = t.t.day; 411 | self.night.brght = t.t.night; 412 | } 413 | if let Some(t) = gamma { 414 | self.day.gamma = t.t.day; 415 | self.night.gamma = t.t.night; 416 | } 417 | 418 | if let Some(t) = reset_ramps { 419 | self.reset_ramps = t; 420 | } 421 | if let Some(t) = scheme { 422 | self.scheme = t; 423 | } 424 | if let Some(t) = disable_fade { 425 | self.disable_fade = t; 426 | } 427 | 428 | if let Some(t) = sleep_duration_short { 429 | self.sleep_duration_short = Duration::from_millis(t as u64); 430 | } 431 | if let Some(t) = sleep_duration { 432 | self.sleep_duration = Duration::from_millis(t as u64); 433 | } 434 | 435 | if let Some(t) = location { 436 | self.location = t; 437 | } 438 | if let Some(t) = method { 439 | self.method = Some(t); 440 | } 441 | } 442 | } 443 | 444 | impl ConfigFile { 445 | fn new(config_path: Option<&Path>) -> Result { 446 | #[cfg(unix)] 447 | let system_config = 448 | Path::new(formatcp!("/etc/{PKG_NAME}/config.toml")); 449 | let local_config = 450 | dirs::config_dir().map(|d| d.join(PKG_NAME).join("config.toml")); 451 | let user_config = config_path 452 | .map(|p| match p.is_file() { 453 | true => Ok(p), 454 | false => Err(ConfigFileError::PathNotFile(p.into())), 455 | }) 456 | .transpose()? 457 | .or(local_config.as_deref()) 458 | .ok_or(ConfigFileError::ConfigDirNotFound)?; 459 | 460 | let mut config = Self::default(); 461 | let mut buf = String::new(); 462 | let mut read = |path: &Path| -> Result<(), ConfigFileError> { 463 | if path.is_file() { 464 | (|| File::open(path)?.read_to_string(&mut buf))().map_err( 465 | |e| ConfigFileError::OpenFailed(e, path.into()), 466 | )?; 467 | let cfg = toml::from_str(&buf).map_err(|e| { 468 | ConfigFileError::DeserializeFailed(e, path.into()) 469 | })?; 470 | config.merge(cfg); 471 | Ok(()) 472 | } else { 473 | Ok(()) 474 | } 475 | }; 476 | 477 | #[cfg(unix)] 478 | read(system_config)?; 479 | read(user_config)?; 480 | Ok(config) 481 | } 482 | 483 | fn merge(&mut self, other: Self) { 484 | let Self { 485 | temperature, 486 | brightness, 487 | gamma, 488 | reset_ramps, 489 | scheme, 490 | disable_fade, 491 | sleep_duration_short, 492 | sleep_duration, 493 | method, 494 | location, 495 | } = other; 496 | 497 | if let Some(t) = temperature { 498 | self.temperature = Some(t); 499 | } 500 | if let Some(t) = brightness { 501 | self.brightness = Some(t); 502 | } 503 | if let Some(t) = gamma { 504 | self.gamma = Some(t); 505 | } 506 | self.reset_ramps = reset_ramps; 507 | self.disable_fade = disable_fade; 508 | if let Some(t) = scheme { 509 | self.scheme = Some(t); 510 | } 511 | 512 | if let Some(t) = sleep_duration { 513 | self.sleep_duration = Some(t); 514 | } 515 | if let Some(t) = sleep_duration_short { 516 | self.sleep_duration_short = Some(t); 517 | } 518 | 519 | if let Some(t) = location { 520 | self.location = Some(t); 521 | } 522 | if let Some(t) = method { 523 | self.method = Some(t); 524 | } 525 | } 526 | } 527 | 528 | // 529 | 530 | impl Default for ConfigBuilder { 531 | fn default() -> Self { 532 | Self { 533 | day: ColorSettings::default_day(), 534 | night: ColorSettings::default_night(), 535 | mode: Default::default(), 536 | reset_ramps: Default::default(), 537 | scheme: Default::default(), 538 | disable_fade: Default::default(), 539 | sleep_duration_short: Duration::from_millis( 540 | DEFAULT_SLEEP_DURATION_SHORT, 541 | ), 542 | sleep_duration: Duration::from_millis(DEFAULT_SLEEP_DURATION), 543 | method: Default::default(), 544 | location: Default::default(), 545 | } 546 | } 547 | } 548 | 549 | impl From for ColorSettings { 550 | fn from(t: ColorSettingsArgs) -> Self { 551 | let ColorSettingsArgs { 552 | temperature, 553 | gamma, 554 | brightness, 555 | } = t; 556 | 557 | Self { 558 | temp: temperature, 559 | gamma, 560 | brght: brightness, 561 | } 562 | } 563 | } 564 | 565 | impl<'de, T, U> Deserialize<'de> for Either 566 | where 567 | T: Deserialize<'de>, 568 | U: Deserialize<'de> + TryInto, 569 | U::Error: Display, 570 | { 571 | fn deserialize>(d: D) -> Result { 572 | let v = Value::deserialize(d)?; 573 | let t = match U::deserialize(v.clone()) { 574 | Ok(u) => u.try_into().map_err(de::Error::custom)?, 575 | Err(_) => match T::deserialize(v) { 576 | Ok(t) => t, 577 | Err(e) => Err(de::Error::custom(e))?, 578 | }, 579 | }; 580 | 581 | Ok(Self { t, p: PhantomData }) 582 | } 583 | } 584 | 585 | impl<'de, E, T> Deserialize<'de> for DayNight 586 | where 587 | E: DayNightErrorType, 588 | T: Clone + FromStr, 589 | { 590 | fn deserialize>(d: D) -> Result { 591 | String::deserialize(d)?.parse().map_err(de::Error::custom) 592 | } 593 | } 594 | 595 | impl<'de> Deserialize<'de> for TransitionScheme { 596 | fn deserialize>(d: D) -> Result { 597 | String::deserialize(d)?.parse().map_err(de::Error::custom) 598 | } 599 | } 600 | 601 | impl<'de> Deserialize<'de> for LocationProviderType { 602 | fn deserialize>(d: D) -> Result { 603 | String::deserialize(d)?.parse().map_err(de::Error::custom) 604 | } 605 | } 606 | 607 | impl<'de> Deserialize<'de> for AdjustmentMethodType { 608 | fn deserialize>(d: D) -> Result { 609 | String::deserialize(d)?.parse().map_err(de::Error::custom) 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /src/gamma_drm.rs: -------------------------------------------------------------------------------- 1 | /* gamma-drm.rs -- Direct Rendering Manager gamma adjustment 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2014 Mattias Andrée 6 | Copyright (c) 2017 Jon Lund Steffensen 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | */ 21 | 22 | use crate::{ 23 | calc_colorramp::GammaRamps, 24 | error::{ 25 | gamma::{CrtcError, DrmCrtcError, DrmError}, 26 | AdjusterError, AdjusterErrorInner, 27 | }, 28 | types::ColorSettings, 29 | utils::CollectResult, 30 | Adjuster, 31 | }; 32 | use drm::{ 33 | control::{ 34 | crtc::Handle as CrtcHandle, from_u32 as handle_from_u32, 35 | Device as ControlDevice, 36 | }, 37 | Device, 38 | }; 39 | use std::{ 40 | fs::{File, OpenOptions}, 41 | io, 42 | os::fd::{AsFd, BorrowedFd}, 43 | path::Path, 44 | }; 45 | 46 | #[derive(Debug)] 47 | struct Card(File); 48 | 49 | #[derive(Debug)] 50 | pub struct Drm { 51 | card: Card, 52 | crtcs: Vec, 53 | } 54 | 55 | #[derive(Debug)] 56 | struct Crtc { 57 | handle: CrtcHandle, 58 | ramp_size: u32, 59 | saved_ramps: GammaRamps, 60 | } 61 | 62 | impl AsFd for Card { 63 | fn as_fd(&self) -> BorrowedFd<'_> { 64 | self.0.as_fd() 65 | } 66 | } 67 | 68 | impl Device for Card {} 69 | impl ControlDevice for Card {} 70 | 71 | impl Card { 72 | pub fn open(path: impl AsRef) -> Result { 73 | fn inner(path: &Path) -> Result { 74 | let mut options = OpenOptions::new(); 75 | options.read(true); 76 | options.write(true); 77 | Ok(Card( 78 | options 79 | .open(path) 80 | .map_err(|e| DrmError::OpenDeviceFailed(e, path.into()))?, 81 | )) 82 | } 83 | inner(path.as_ref()) 84 | } 85 | } 86 | 87 | impl Drm { 88 | pub fn new( 89 | card_num: Option, 90 | crtc_ids: Vec, 91 | ) -> Result { 92 | let path = format!("/dev/dri/card{}", card_num.unwrap_or_default()); 93 | let card = Card::open(path)?; 94 | let crtcs = Self::get_crtcs(&card, crtc_ids)?; 95 | Ok(Self { card, crtcs }) 96 | } 97 | 98 | fn get_crtcs( 99 | card: &Card, 100 | mut crtc_ids: Vec, 101 | ) -> Result, DrmError> { 102 | let all_crtcs = card 103 | .resource_handles() 104 | .map_err(DrmError::GetResourcesFailed)? 105 | .crtcs; 106 | 107 | let crtcs = if crtc_ids.is_empty() { 108 | all_crtcs 109 | } else { 110 | let len = crtc_ids.len(); 111 | crtc_ids.sort(); 112 | crtc_ids.dedup(); 113 | if len > crtc_ids.len() { 114 | Err(DrmError::NonUniqueCrtc)? 115 | } 116 | let f = |h| Self::validate_crtc(&all_crtcs, h); 117 | crtc_ids.into_iter().map(f).collect::, _>>()? 118 | }; 119 | 120 | crtcs 121 | .into_iter() 122 | .map(|h| Self::get_crtc(card, h)) 123 | .collect_result() 124 | .map_err(DrmError::GetCrtcs) 125 | } 126 | 127 | fn validate_crtc( 128 | all_crtcs: &[CrtcHandle], 129 | id: u32, 130 | ) -> Result { 131 | let crtcs = 132 | || all_crtcs.iter().map(|&h| h.into()).collect::>(); 133 | let handle: CrtcHandle = 134 | handle_from_u32(id).ok_or(DrmError::ZeroValueCrtc)?; 135 | if all_crtcs.iter().any(|&h| handle == h) { 136 | Ok(handle) 137 | } else { 138 | Err(DrmError::InvalidCrtc(crtcs())) 139 | } 140 | } 141 | 142 | fn get_crtc( 143 | card: &Card, 144 | handle: CrtcHandle, 145 | ) -> Result> { 146 | let f = || -> Result { 147 | let info = card 148 | .get_crtc(handle) 149 | .map_err(DrmCrtcError::GetRampSizeFailed)?; 150 | let ramp_size = info.gamma_length(); 151 | if ramp_size <= 1 { 152 | Err(DrmCrtcError::InvalidRampSize(ramp_size))? 153 | } 154 | 155 | let (mut r, mut g, mut b) = (Vec::new(), Vec::new(), Vec::new()); 156 | // FIX: Error: Bad address (os error 14) 157 | // drm_ffi::mode::get_gamma( 158 | // card.as_fd(), 159 | // handle.into(), 160 | // ramp_size as usize, 161 | // &mut r, 162 | // &mut g, 163 | // &mut b, 164 | // )?; 165 | // 166 | // The C function drmModeCrtcGetGamma works on my system 167 | // Test here: https://github.com/mahor1221/redshift 168 | // build and run: ./redshift -m drm:card= -x 169 | // 170 | // everything is similar to the C function, why it doesn't work 171 | // https://gitlab.freedesktop.org/mesa/drm/-/blob/main/xf86drmMode.c#L1000 172 | // https://gitlab.freedesktop.org/mesa/drm/-/blob/main/include/drm/drm.h#L1155 173 | // 174 | // FIX: Error: Invalid argument (os error 22) 175 | card.get_gamma(handle, &mut r, &mut g, &mut b) 176 | .map_err(DrmCrtcError::GetRampFailed)?; 177 | let saved_ramps = GammaRamps([r, g, b]); 178 | // _("DRM could not read gamma ramps on CRTC %i on\n" 179 | // "graphics card %i, ignoring device.\n"), 180 | 181 | Ok(Crtc { 182 | handle, 183 | ramp_size, 184 | saved_ramps, 185 | }) 186 | }; 187 | 188 | f().map_err(|err| CrtcError { 189 | id: handle.into(), 190 | err, 191 | }) 192 | } 193 | 194 | fn set_gamma_ramps( 195 | &self, 196 | f: impl Fn(&Crtc) -> io::Result<()>, 197 | ) -> Result<(), AdjusterErrorInner> { 198 | self.crtcs.iter().map(f).collect_result()?; 199 | Ok(()) 200 | } 201 | } 202 | 203 | impl Adjuster for Drm { 204 | fn restore(&self) -> Result<(), AdjusterError> { 205 | self.set_gamma_ramps(|crtc| { 206 | self.card.set_gamma( 207 | crtc.handle, 208 | &crtc.saved_ramps[0], 209 | &crtc.saved_ramps[1], 210 | &crtc.saved_ramps[2], 211 | ) 212 | }) 213 | .map_err(AdjusterError::Restore) 214 | } 215 | 216 | fn set( 217 | &self, 218 | reset_ramps: bool, 219 | cs: &ColorSettings, 220 | ) -> Result<(), AdjusterError> { 221 | self.set_gamma_ramps(|crtc| { 222 | let mut ramps = if reset_ramps { 223 | GammaRamps::new(crtc.ramp_size) 224 | } else { 225 | crtc.saved_ramps.clone() 226 | }; 227 | 228 | ramps.colorramp_fill(cs); 229 | self.card 230 | .set_gamma(crtc.handle, &ramps[0], &ramps[1], &ramps[2]) 231 | }) 232 | .map_err(AdjusterError::Set) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/gamma_dummy.rs: -------------------------------------------------------------------------------- 1 | /* gamma-dummy.rs -- No-op gamma adjustment 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2013-2017 Jon Lund Steffensen 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | use crate::{error::AdjusterError, types::ColorSettings, Adjuster}; 22 | 23 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 24 | pub struct Dummy; 25 | impl Adjuster for Dummy { 26 | fn restore(&self) -> Result<(), AdjusterError> { 27 | Ok(()) 28 | } 29 | 30 | fn set( 31 | &self, 32 | _reset_ramps: bool, 33 | _cs: &ColorSettings, 34 | ) -> Result<(), AdjusterError> { 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/gamma_randr.rs: -------------------------------------------------------------------------------- 1 | /* gamma-randr.rs -- X RANDR gamma adjustment 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2010-2017 Jon Lund Steffensen 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | use crate::{ 22 | calc_colorramp::GammaRamps, 23 | config::{RANDR_MAJOR_VERSION, RANDR_MINOR_VERSION_MIN}, 24 | error::{ 25 | gamma::{CrtcError, RandrCrtcError, RandrError}, 26 | AdjusterError, AdjusterErrorInner, 27 | }, 28 | types::ColorSettings, 29 | utils::{CollectResult, InjectMapErr}, 30 | Adjuster, 31 | }; 32 | use x11rb::{ 33 | connection::Connection as _, 34 | cookie::{Cookie, VoidCookie}, 35 | errors::ConnectionError, 36 | protocol::randr::{ 37 | ConnectionExt, GetCrtcGammaReply, GetCrtcGammaSizeReply, 38 | }, 39 | rust_connection::RustConnection as Conn, 40 | }; 41 | 42 | #[derive(Debug)] 43 | pub struct Randr { 44 | conn: Conn, 45 | crtcs: Vec, 46 | } 47 | 48 | #[derive(Debug)] 49 | struct Crtc { 50 | id: u32, 51 | ramp_size: u16, 52 | saved_ramps: GammaRamps, 53 | } 54 | 55 | impl Randr { 56 | pub fn new( 57 | screen_num: Option, 58 | crtc_ids: Vec, 59 | ) -> Result { 60 | // uses the DISPLAY environment variable if screen_num is None 61 | let screen_num = screen_num.map(|n| ":".to_string() + &n.to_string()); 62 | let (conn, screen_num) = x11rb::connect(screen_num.as_deref())?; 63 | 64 | // returns a lower version if 1.3 is not supported 65 | let r = conn 66 | .randr_query_version(1, 3) 67 | .inject_map_err(RandrError::GetVersionFailed)? 68 | .reply() 69 | .inject_map_err(RandrError::GetVersionFailed)?; 70 | 71 | // eprintln!("`{}` returned error {}", "RANDR Query Version", ec); 72 | if r.major_version != RANDR_MAJOR_VERSION 73 | || r.minor_version < RANDR_MINOR_VERSION_MIN 74 | { 75 | Err(RandrError::UnsupportedVersion { 76 | major: r.major_version, 77 | minor: r.minor_version, 78 | })? 79 | } 80 | 81 | let crtcs = Self::get_crtcs(&conn, screen_num, crtc_ids)?; 82 | 83 | Ok(Self { conn, crtcs }) 84 | } 85 | 86 | fn get_crtcs( 87 | conn: &Conn, 88 | screen_num: usize, 89 | mut crtc_ids: Vec, 90 | ) -> Result, RandrError> { 91 | let win = conn.setup().roots[screen_num].root; 92 | let all_crtcs = conn 93 | .randr_get_screen_resources_current(win) 94 | .inject_map_err(RandrError::GetResourcesFailed)? 95 | .reply() 96 | .inject_map_err(RandrError::GetResourcesFailed)? 97 | .crtcs; 98 | 99 | let crtcs = if crtc_ids.is_empty() { 100 | all_crtcs 101 | } else { 102 | let len = crtc_ids.len(); 103 | crtc_ids.sort(); 104 | crtc_ids.dedup(); 105 | if len > crtc_ids.len() { 106 | Err(RandrError::NonUniqueCrtc)? 107 | } 108 | let f = |&h| Self::validate_crtc(&all_crtcs, h); 109 | crtc_ids.iter().try_for_each(f)?; 110 | crtc_ids 111 | }; 112 | 113 | crtcs 114 | .into_iter() 115 | .map(|id| { 116 | let c_ramp = conn.randr_get_crtc_gamma(id)?; 117 | let c_size = conn.randr_get_crtc_gamma_size(id)?; 118 | Ok((id, c_size, c_ramp)) 119 | }) 120 | // collect to send all of the requests 121 | .collect_result() 122 | .map_err(RandrError::SendRequestFailed)? 123 | .into_iter() 124 | .map(Self::get_crtc) 125 | .collect_result() 126 | .map_err(RandrError::GetCrtcs) 127 | } 128 | 129 | fn validate_crtc(all_crtcs: &[u32], id: u32) -> Result<(), RandrError> { 130 | if all_crtcs.iter().any(|&i| id == i) { 131 | Ok(()) 132 | } else { 133 | Err(RandrError::InvalidCrtc(all_crtcs.to_vec())) 134 | } 135 | } 136 | 137 | fn get_crtc( 138 | (id, c_size, c_ramp): ( 139 | u32, 140 | Cookie, 141 | Cookie, 142 | ), 143 | ) -> Result> { 144 | let f = || -> Result { 145 | let r = c_ramp.reply().map_err(RandrCrtcError::GetRampFailed)?; 146 | let saved_ramps = GammaRamps([r.red, r.green, r.blue]); 147 | let ramp_size = c_size 148 | .reply() 149 | .map_err(RandrCrtcError::GetRampSizeFailed)? 150 | .size; 151 | if ramp_size == 0 { 152 | Err(RandrCrtcError::InvalidRampSize(ramp_size))? 153 | } 154 | 155 | Ok(Crtc { 156 | id, 157 | ramp_size, 158 | saved_ramps, 159 | }) 160 | }; 161 | 162 | f().map_err(|err| CrtcError { id, err }) 163 | } 164 | 165 | fn set_gamma_ramps<'s>( 166 | &'s self, 167 | f: impl Fn(&Crtc) -> Result, ConnectionError>, 168 | ) -> Result<(), AdjusterErrorInner> { 169 | self.crtcs 170 | .iter() 171 | .map(f) 172 | // collect to send all of the requests 173 | .collect_result() 174 | .inject_map_err(AdjusterErrorInner::Randr)? 175 | .into_iter() 176 | .map(|c| c.check()) 177 | .collect_result() 178 | .inject_map_err(AdjusterErrorInner::Randr)?; 179 | Ok(()) 180 | } 181 | } 182 | 183 | impl Adjuster for Randr { 184 | fn restore(&self) -> Result<(), AdjusterError> { 185 | self.set_gamma_ramps(|crtc| { 186 | self.conn.randr_set_crtc_gamma( 187 | crtc.id, 188 | &crtc.saved_ramps[0], 189 | &crtc.saved_ramps[1], 190 | &crtc.saved_ramps[2], 191 | ) 192 | }) 193 | .map_err(AdjusterError::Restore) 194 | } 195 | 196 | fn set( 197 | &self, 198 | reset_ramps: bool, 199 | cs: &ColorSettings, 200 | ) -> Result<(), AdjusterError> { 201 | self.set_gamma_ramps(|crtc| { 202 | let mut ramps = if reset_ramps { 203 | GammaRamps::new(crtc.ramp_size as u32) 204 | } else { 205 | crtc.saved_ramps.clone() 206 | }; 207 | 208 | ramps.colorramp_fill(cs); 209 | self.conn 210 | .randr_set_crtc_gamma(crtc.id, &ramps[0], &ramps[1], &ramps[2]) 211 | }) 212 | .map_err(AdjusterError::Restore) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/gamma_vidmode.rs: -------------------------------------------------------------------------------- 1 | /* gamma-vidmode.rs -- X VidMode gamma adjustment 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2010-2017 Jon Lund Steffensen 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | use crate::{ 22 | calc_colorramp::GammaRamps, 23 | error::{gamma::VidmodeError, AdjusterError, AdjusterErrorInner}, 24 | types::ColorSettings, 25 | utils::InjectMapErr, 26 | Adjuster, 27 | }; 28 | use x11rb::{ 29 | protocol::xf86vidmode::ConnectionExt, 30 | rust_connection::RustConnection as X11Connection, 31 | }; 32 | 33 | #[derive(Debug)] 34 | pub struct Vidmode { 35 | conn: X11Connection, 36 | screen_num: u16, 37 | ramp_size: u16, 38 | saved_ramps: GammaRamps, 39 | } 40 | 41 | impl Vidmode { 42 | pub fn new(screen_num: Option) -> Result { 43 | // it uses the DISPLAY environment variable if screen_num is None 44 | let screen_num = screen_num.map(|n| ":".to_string() + &n.to_string()); 45 | let (conn, screen_num) = x11rb::connect(screen_num.as_deref())?; 46 | let screen_num = screen_num as u16; 47 | 48 | // check connection 49 | conn.xf86vidmode_query_version() 50 | .inject_map_err(VidmodeError::GetVersionFailed)? 51 | .reply() 52 | .inject_map_err(VidmodeError::GetVersionFailed)?; 53 | 54 | let ramp_size = conn 55 | .xf86vidmode_get_gamma_ramp_size(screen_num) 56 | .inject_map_err(VidmodeError::GetRampSizeFailed)? 57 | .reply() 58 | .inject_map_err(VidmodeError::GetRampSizeFailed)? 59 | .size; 60 | 61 | if ramp_size == 0 { 62 | Err(VidmodeError::InvalidRampSize(ramp_size))? 63 | } 64 | 65 | let ramp = conn 66 | .xf86vidmode_get_gamma_ramp(screen_num, ramp_size) 67 | .inject_map_err(VidmodeError::GetRampSizeFailed)? 68 | .reply() 69 | .inject_map_err(VidmodeError::GetRampSizeFailed)?; 70 | // eprintln!("X request failed: XF86VidModeGetGammaRamp"); 71 | let saved_ramps = GammaRamps([ramp.red, ramp.green, ramp.blue]); 72 | 73 | Ok(Self { 74 | conn, 75 | screen_num, 76 | ramp_size, 77 | saved_ramps, 78 | }) 79 | } 80 | 81 | fn set_gamma_ramps( 82 | &self, 83 | ramps: &GammaRamps, 84 | ) -> Result<(), AdjusterErrorInner> { 85 | self.conn 86 | .xf86vidmode_set_gamma_ramp( 87 | self.screen_num, 88 | self.ramp_size, 89 | &ramps[0], 90 | &ramps[1], 91 | &ramps[2], 92 | ) 93 | .inject_map_err(AdjusterErrorInner::Vidmode)? 94 | .check() 95 | .inject_map_err(AdjusterErrorInner::Vidmode)?; 96 | Ok(()) 97 | } 98 | } 99 | 100 | impl Adjuster for Vidmode { 101 | fn restore(&self) -> Result<(), AdjusterError> { 102 | self.set_gamma_ramps(&self.saved_ramps) 103 | .map_err(AdjusterError::Restore) 104 | } 105 | 106 | fn set( 107 | &self, 108 | reset_ramps: bool, 109 | cs: &ColorSettings, 110 | ) -> Result<(), AdjusterError> { 111 | let mut ramps = if reset_ramps { 112 | GammaRamps::new(self.ramp_size as u32) 113 | } else { 114 | self.saved_ramps.clone() 115 | }; 116 | 117 | ramps.colorramp_fill(cs); 118 | self.set_gamma_ramps(&ramps).map_err(AdjusterError::Set) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/gamma_win32gdi.rs: -------------------------------------------------------------------------------- 1 | /* gamma-vidmode.rs -- Windows GDI gamma adjustment 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2010-2017 Jon Lund Steffensen 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | #![allow(unsafe_code)] 22 | #![allow(clippy::undocumented_unsafe_blocks)] 23 | 24 | use crate::{ 25 | calc_colorramp::GammaRampsWin32, 26 | error::{gamma::Win32GdiError, AdjusterError, AdjusterErrorInner}, 27 | types::ColorSettings, 28 | Adjuster, 29 | }; 30 | use std::ffi::c_void; 31 | use windows::Win32::{ 32 | Foundation::HWND, 33 | Graphics::Gdi::{GetDC, GetDeviceCaps, ReleaseDC, COLORMGMTCAPS}, 34 | UI::ColorSystem::{GetDeviceGammaRamp, SetDeviceGammaRamp}, 35 | }; 36 | 37 | const MAX_ATTEMPTS: u8 = 10; 38 | const GAMMA_RAMP_SIZE: usize = 256; 39 | 40 | // https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types 41 | type Word = u16; 42 | // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc 43 | const HWND_NULL: HWND = HWND(0); 44 | 45 | #[derive(Debug)] 46 | pub struct Win32Gdi { 47 | saved_ramps: GammaRampsWin32, 48 | } 49 | 50 | impl Win32Gdi { 51 | pub fn new() -> Result { 52 | unsafe { 53 | // Open device context 54 | let hdc = GetDC(HWND_NULL); 55 | if hdc.is_invalid() { 56 | Err(Win32GdiError::GetDCFailed)?; 57 | } 58 | 59 | // Check support for gamma ramps 60 | let cmcap = GetDeviceCaps(hdc, COLORMGMTCAPS); 61 | if cmcap as i64 != COLORMGMTCAPS.0 as i64 { 62 | ReleaseDC(HWND_NULL, hdc); 63 | Err(Win32GdiError::NotSupported)?; 64 | } 65 | 66 | // Save current gamma ramps so we can restore them at program exit 67 | let saved_ramps = { 68 | // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getdevicegammaramp 69 | // saved_ramps = malloc(3*GAMMA_RAMP_SIZE*sizeof(WORD)); 70 | let mut saved_ramps: [[Word; GAMMA_RAMP_SIZE]; 3] = 71 | [[0; GAMMA_RAMP_SIZE]; 3]; 72 | 73 | let ptr = saved_ramps.as_mut_ptr() as *mut c_void; 74 | if GetDeviceGammaRamp(hdc, ptr).0 == 0 { 75 | ReleaseDC(HWND_NULL, hdc); 76 | Err(Win32GdiError::GetRampFailed)?; 77 | } 78 | GammaRampsWin32(Box::new(saved_ramps)) 79 | }; 80 | 81 | ReleaseDC(HWND_NULL, hdc); 82 | Ok(Self { saved_ramps }) 83 | } 84 | } 85 | 86 | fn set_gamma_ramps( 87 | &self, 88 | ramps: &GammaRampsWin32, 89 | ) -> Result<(), AdjusterErrorInner> { 90 | unsafe { 91 | // Open device context 92 | let hdc = GetDC(HWND_NULL); 93 | if hdc.is_invalid() { 94 | Err(Win32GdiError::GetDCFailed)?; 95 | } 96 | 97 | // We retry a few times before giving up because some buggy drivers 98 | // fail on the first invocation of SetDeviceGammaRamp just to 99 | // succeed on the second 100 | let mut i = 0; 101 | let mut err = true; 102 | while i < MAX_ATTEMPTS && err { 103 | i += 1; 104 | err = { 105 | let ptr = ramps.0.as_ptr() as *const c_void; 106 | SetDeviceGammaRamp(hdc, ptr).0 == 0 107 | }; 108 | } 109 | if err { 110 | Err(Win32GdiError::SetRampFailed)? 111 | } 112 | 113 | ReleaseDC(HWND_NULL, hdc); 114 | Ok(()) 115 | } 116 | } 117 | } 118 | 119 | impl Adjuster for Win32Gdi { 120 | fn restore(&self) -> Result<(), AdjusterError> { 121 | self.set_gamma_ramps(&self.saved_ramps) 122 | .map_err(AdjusterError::Restore) 123 | } 124 | 125 | fn set( 126 | &self, 127 | reset_ramps: bool, 128 | cs: &ColorSettings, 129 | ) -> Result<(), AdjusterError> { 130 | let mut ramps = if reset_ramps { 131 | GammaRampsWin32::new() 132 | } else { 133 | self.saved_ramps.clone() 134 | }; 135 | 136 | ramps.colorramp_fill(cs); 137 | self.set_gamma_ramps(&ramps).map_err(AdjusterError::Set) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* redshift.rs -- Main program 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2009-2017 Jon Lund Steffensen 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | // TODO: add tldr page: https://github.com/tldr-pages/tldr 22 | // TODO: add setting screen brightness, a percentage of the current brightness 23 | // see: https://github.com/qualiaa/redshift-hooks 24 | // TODO: ? benchmark: https://github.com/nvzqz/divan 25 | // TODO: Fix large fade steps 26 | // TODO: ? Box large errors 27 | // TODO: move coproduct.rs to a fork of frunk after Error got stabled in core 28 | // see: https://github.com/rust-lang/rust/issues/103765 29 | // TODO: add unit tests 30 | // TODO: check if another instance is running 31 | // TODO: add test for man page 32 | // TODO: fix all document warnings 33 | 34 | mod calc_colorramp; 35 | mod calc_solar; 36 | mod cli; 37 | mod config; 38 | mod coproduct; 39 | mod error; 40 | 41 | #[cfg(unix_without_macos)] 42 | mod gamma_drm; 43 | #[cfg(unix_without_macos)] 44 | mod gamma_randr; 45 | #[cfg(unix_without_macos)] 46 | mod gamma_vidmode; 47 | 48 | #[cfg(windows)] 49 | mod gamma_win32gdi; 50 | 51 | mod gamma_dummy; 52 | mod location_manual; 53 | mod types; 54 | mod types_display; 55 | mod types_parse; 56 | mod utils; 57 | 58 | #[cfg(windows)] 59 | use crate::gamma_win32gdi::Win32Gdi; 60 | #[cfg(unix_without_macos)] 61 | use crate::{gamma_drm::Drm, gamma_randr::Randr, gamma_vidmode::Vidmode}; 62 | pub use cli::cli_args_command; 63 | use error::ReddishError; 64 | use gamma_dummy::Dummy; 65 | use itertools::Itertools; 66 | use location_manual::Manual; 67 | use types::Location; 68 | 69 | use crate::{ 70 | cli::ClapColorChoiceExt, 71 | config::{Config, ConfigBuilder, FADE_STEPS}, 72 | error::{AdjusterError, ProviderError}, 73 | types::{ColorSettings, Elevation, Mode, Period, PeriodInfo}, 74 | types_display::{BODY, HEADER}, 75 | }; 76 | use anstream::AutoStream; 77 | use chrono::{DateTime, SubsecRound, TimeDelta}; 78 | use std::{ 79 | fmt::Debug, 80 | io, 81 | sync::mpsc::{self, Receiver, RecvTimeoutError}, 82 | }; 83 | use tracing::{error, info, Level}; 84 | use tracing_subscriber::fmt::writer::MakeWriterExt; 85 | 86 | pub fn main() { 87 | (|| -> Result<(), ReddishError> { 88 | let c = ConfigBuilder::new(|verbosity, color| { 89 | let choice = color.to_choice(); 90 | let stdout = move || AutoStream::new(io::stdout(), choice).lock(); 91 | let stderr = move || AutoStream::new(io::stderr(), choice).lock(); 92 | let stdio = stderr.with_max_level(Level::WARN).or_else(stdout); 93 | 94 | tracing_subscriber::fmt() 95 | .with_writer(stdio) 96 | .with_max_level(verbosity.level_filter()) 97 | .without_time() 98 | .with_level(false) 99 | .with_target(false) 100 | .init(); 101 | })? 102 | .build()?; 103 | 104 | let (tx, rx) = mpsc::channel(); 105 | ctrlc::set_handler(move || { 106 | #[allow(clippy::expect_used)] 107 | tx.send(()).expect("Could not send signal on channel") 108 | }) 109 | .or_else(|e| match c.mode { 110 | Mode::Oneshot | Mode::Set | Mode::Reset | Mode::Print => Ok(()), 111 | Mode::Daemon => Err(e), 112 | })?; 113 | 114 | run(&c, &rx) 115 | })() 116 | .unwrap_or_else(|e| error!("{e}")) 117 | } 118 | 119 | fn run(c: &Config, sig: &Receiver<()>) -> Result<(), ReddishError> { 120 | match c.mode { 121 | Mode::Daemon => { 122 | info!("{c}\n{HEADER}Current{HEADER:#}:"); 123 | DaemonMode::new(c, sig).run_loop()?; 124 | c.method.restore()?; 125 | } 126 | Mode::Oneshot => { 127 | // Use period and transition progress to set color temperature 128 | let (p, i) = Period::from(&c.scheme, &c.location, c.time)?; 129 | let interp = c.night.interpolate_with(&c.day, p.into()); 130 | info!("{c}\n{HEADER}Current{HEADER:#}:\n{p}\n{i}\n{interp}"); 131 | c.method.set(c.reset_ramps, &interp)?; 132 | } 133 | Mode::Set => { 134 | // for this command, color settings are stored in the day field 135 | c.method.set(c.reset_ramps, &c.day)?; 136 | } 137 | Mode::Reset => { 138 | c.method.set(true, &ColorSettings::default())?; 139 | } 140 | Mode::Print => run_print_mode(c)?, 141 | } 142 | 143 | Ok(()) 144 | } 145 | 146 | fn run_print_mode(c: &Config) -> Result<(), ReddishError> { 147 | let now = (c.time)(); 148 | let delta = now.to_utc() - DateTime::UNIX_EPOCH; 149 | let loc = c.location.get()?; 150 | let mut buf = (0..24).map(|h| { 151 | let d = TimeDelta::hours(h); 152 | let time = (now + d).time().trunc_subsecs(0); 153 | let elev = Elevation::new((delta + d).num_seconds() as f64, loc); 154 | format!("{BODY}{time}{BODY:#}: {:6.2}°", *elev) 155 | }); 156 | Ok(info!("{}", buf.join("\n"))) 157 | } 158 | 159 | #[derive(Debug)] 160 | struct DaemonMode<'a, 'b> { 161 | cfg: &'a Config, 162 | sig: &'b Receiver<()>, 163 | 164 | signal: Signal, 165 | fade: FadeStatus, 166 | 167 | period: Period, 168 | info: PeriodInfo, 169 | interp: ColorSettings, 170 | 171 | // Save previous parameters so we can avoid printing status updates if the 172 | // values did not change 173 | prev_period: Option, 174 | prev_info: Option, 175 | prev_interp: Option, 176 | } 177 | 178 | impl<'a, 'b> DaemonMode<'a, 'b> { 179 | fn new(cfg: &'a Config, sig: &'b Receiver<()>) -> Self { 180 | Self { 181 | cfg, 182 | sig, 183 | signal: Default::default(), 184 | fade: Default::default(), 185 | period: Default::default(), 186 | info: Default::default(), 187 | interp: Default::default(), 188 | prev_period: Default::default(), 189 | prev_info: Default::default(), 190 | prev_interp: Default::default(), 191 | } 192 | } 193 | 194 | /// This is the main loop of the daemon mode which keeps track of the 195 | /// current time and continuously updates the screen to the appropriate 196 | /// color temperature 197 | fn run_loop(&mut self) -> Result<(), ReddishError> { 198 | let c = self.cfg; 199 | loop { 200 | (self.period, self.info) = 201 | Period::from(&c.scheme, &c.location, c.time)?; 202 | 203 | let target = match self.signal { 204 | Signal::None => { 205 | c.night.interpolate_with(&c.day, self.period.into()) 206 | } 207 | Signal::Interrupt => ColorSettings::default(), 208 | }; 209 | 210 | (self.interp, self.fade) = self.next_interpolate(target); 211 | 212 | self.log(); 213 | 214 | // // Activate hooks if period changed 215 | // if period != prev_period { 216 | // hooks_signal_period_change(prev_period, period); 217 | // } 218 | 219 | c.method.set(c.reset_ramps, &self.interp)?; 220 | 221 | self.prev_period = Some(self.period); 222 | self.prev_info = Some(self.info.clone()); 223 | self.prev_interp = Some(self.interp.clone()); 224 | 225 | // sleep for a duration then continue the loop 226 | // or wake up and restore the default colors slowly on first ctrl-c 227 | // or break the loop on the second ctrl-c immediately 228 | let sleep_duration = match (self.signal, self.fade) { 229 | (Signal::None, FadeStatus::Completed) => c.sleep_duration, 230 | (_, FadeStatus::Ungoing { .. }) => c.sleep_duration_short, 231 | (Signal::Interrupt, FadeStatus::Completed) => break Ok(()), 232 | }; 233 | 234 | match self.sig.recv_timeout(sleep_duration) { 235 | Err(RecvTimeoutError::Timeout) => {} 236 | Err(e) => Err(e)?, 237 | Ok(()) => match self.signal { 238 | Signal::None => self.signal = Signal::Interrupt, 239 | Signal::Interrupt => break Ok(()), 240 | }, 241 | } 242 | } 243 | } 244 | 245 | fn next_interpolate( 246 | &self, 247 | target: ColorSettings, 248 | ) -> (ColorSettings, FadeStatus) { 249 | use FadeStatus::*; 250 | let target_is_very_different = self.interp.is_very_diff_from(&target); 251 | match (&self.fade, target_is_very_different, self.cfg.disable_fade) { 252 | (_, _, true) | (Completed | Ungoing { .. }, false, false) => { 253 | (target, Completed) 254 | } 255 | 256 | (Completed, true, false) => { 257 | let next = Self::interpolate(&self.interp, &target, 0); 258 | (next, Ungoing { step: 0 }) 259 | } 260 | 261 | (Ungoing { step }, true, false) => { 262 | if *step < FADE_STEPS { 263 | let step = *step + 1; 264 | let next = Self::interpolate(&self.interp, &target, step); 265 | (next, Ungoing { step }) 266 | } else { 267 | (target, Completed) 268 | } 269 | } 270 | } 271 | } 272 | 273 | fn interpolate( 274 | start: &ColorSettings, 275 | end: &ColorSettings, 276 | step: u8, 277 | ) -> ColorSettings { 278 | let frac = step as f64 / FADE_STEPS as f64; 279 | let alpha = Self::ease_fade(frac) 280 | .clamp(0.0, 1.0) 281 | .try_into() 282 | .unwrap_or_else(|_| unreachable!()); 283 | start.interpolate_with(end, alpha) 284 | } 285 | 286 | /// Easing function for fade 287 | /// See https://github.com/mietek/ease-tween 288 | fn ease_fade(t: f64) -> f64 { 289 | if t <= 0.0 { 290 | 0.0 291 | } else if t >= 1.0 { 292 | 1.0 293 | } else { 294 | 1.0042954579734844 295 | * (-6.404173895841566 * (-7.290824133098134 * t).exp()).exp() 296 | } 297 | } 298 | } 299 | 300 | trait Provider { 301 | fn get(&self) -> Result; 302 | } 303 | 304 | trait Adjuster { 305 | /// Restore the adjustment to the state before the Adjuster object was created 306 | fn restore(&self) -> Result<(), AdjusterError>; 307 | /// Set a specific temperature 308 | fn set( 309 | &self, 310 | reset_ramps: bool, 311 | cs: &ColorSettings, 312 | ) -> Result<(), AdjusterError>; 313 | } 314 | 315 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] 316 | enum Signal { 317 | #[default] 318 | None, 319 | Interrupt, 320 | } 321 | 322 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 323 | enum FadeStatus { 324 | Completed, 325 | Ungoing { step: u8 }, 326 | } 327 | 328 | impl Default for FadeStatus { 329 | fn default() -> Self { 330 | Self::Completed 331 | } 332 | } 333 | 334 | // 335 | 336 | #[derive(Debug, PartialEq)] 337 | pub enum LocationProvider { 338 | Manual(Manual), 339 | Geoclue2(Geoclue2), 340 | } 341 | 342 | #[derive(Debug)] 343 | pub enum AdjustmentMethod { 344 | Dummy(Dummy), 345 | #[cfg(unix_without_macos)] 346 | Randr(Randr), 347 | #[cfg(unix_without_macos)] 348 | Drm(Drm), 349 | #[cfg(unix_without_macos)] 350 | Vidmode(Vidmode), 351 | #[cfg(windows)] 352 | Win32Gdi(Win32Gdi), 353 | } 354 | 355 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 356 | pub struct Geoclue2; 357 | 358 | impl Provider for Geoclue2 { 359 | // Listen and handle location updates 360 | // fn fd() -> c_int; 361 | 362 | fn get(&self) -> Result { 363 | // Redshift: "Waiting for current location to become available..." 364 | Err(ProviderError) 365 | } 366 | } 367 | 368 | impl Provider for LocationProvider { 369 | fn get(&self) -> Result { 370 | match self { 371 | Self::Manual(t) => t.get(), 372 | Self::Geoclue2(t) => t.get(), 373 | } 374 | } 375 | } 376 | 377 | impl Adjuster for AdjustmentMethod { 378 | fn restore(&self) -> Result<(), AdjusterError> { 379 | match self { 380 | Self::Dummy(t) => t.restore(), 381 | #[cfg(unix_without_macos)] 382 | Self::Randr(t) => t.restore(), 383 | #[cfg(unix_without_macos)] 384 | Self::Drm(t) => t.restore(), 385 | #[cfg(unix_without_macos)] 386 | Self::Vidmode(t) => t.restore(), 387 | #[cfg(windows)] 388 | Self::Win32Gdi(t) => t.restore(), 389 | } 390 | } 391 | 392 | fn set( 393 | &self, 394 | reset_ramps: bool, 395 | cs: &ColorSettings, 396 | ) -> Result<(), AdjusterError> { 397 | match self { 398 | Self::Dummy(t) => t.set(reset_ramps, cs), 399 | #[cfg(unix_without_macos)] 400 | Self::Randr(t) => t.set(reset_ramps, cs), 401 | #[cfg(unix_without_macos)] 402 | Self::Drm(t) => t.set(reset_ramps, cs), 403 | #[cfg(unix_without_macos)] 404 | Self::Vidmode(t) => t.set(reset_ramps, cs), 405 | #[cfg(windows)] 406 | Self::Win32Gdi(t) => t.set(reset_ramps, cs), 407 | // #[cfg(macos)] 408 | // Self::Quartz(t) => { 409 | // // Redshift: In Quartz (macOS) the gamma adjustments will 410 | // // automatically revert when the process exits Therefore, 411 | // // we have to loop until CTRL-C is received 412 | // if strcmp(options.method.name, "quartz") == 0 { 413 | // println!("Press ctrl-c to stop..."); 414 | // pause(); 415 | // } 416 | // } 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/location_manual.rs: -------------------------------------------------------------------------------- 1 | /* location-manual.rs -- Manual location provider 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | Ported from Redshift . 5 | Copyright (c) 2010-2017 Jon Lund Steffensen 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | use crate::{error::ProviderError, types::Location, Provider}; 22 | 23 | #[derive(Debug, Clone, Default, PartialEq)] 24 | pub struct Manual { 25 | location: Location, 26 | } 27 | 28 | impl Manual { 29 | pub fn new(location: Location) -> Self { 30 | Self { location } 31 | } 32 | } 33 | 34 | impl Provider for Manual { 35 | fn get(&self) -> Result { 36 | Ok(self.location) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | reddish_shift::main() 3 | } 4 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | /* types.rs -- Common types 2 | This file is part of . 3 | Copyright (C) 2024 Mahor Foruzesh 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | use crate::{ 20 | calc_solar::{solar_elevation, SOLAR_CIVIL_TWILIGHT_ELEV}, 21 | error::{types::*, ProviderError}, 22 | utils::{InjectErr, IntoGeneric}, 23 | LocationProvider, Provider, 24 | }; 25 | use chrono::{DateTime, Local, NaiveTime, Timelike}; 26 | use frunk::{validated::IntoValidated, Generic}; 27 | use std::ops::Deref; 28 | 29 | /// Angular elevation of the sun at which the color temperature transition 30 | /// period starts and ends (in degrees). 31 | /// Transition during twilight, and while the sun is lower than 3.0 degrees 32 | /// above the horizon. 33 | pub const DEFAULT_ELEVATION_LOW: f64 = SOLAR_CIVIL_TWILIGHT_ELEV; 34 | pub const DEFAULT_ELEVATION_HIGH: f64 = 3.0; 35 | pub const DEFAULT_LATITUDE: f64 = 0.0; 36 | pub const DEFAULT_LONGITUDE: f64 = 0.0; 37 | pub const DEFAULT_BRIGHTNESS: f64 = 1.0; 38 | pub const DEFAULT_GAMMA: f64 = 1.0; 39 | pub const DEFAULT_TEMPERATURE: u16 = 6500; 40 | pub const DEFAULT_TEMPERATURE_DAY: u16 = 6500; 41 | pub const DEFAULT_TEMPERATURE_NIGHT: u16 = 4500; 42 | 43 | pub const MIN_TEMPERATURE: u16 = 1000; 44 | pub const MAX_TEMPERATURE: u16 = 25000; 45 | pub const MIN_BRIGHTNESS: f64 = 0.1; 46 | pub const MAX_BRIGHTNESS: f64 = 1.0; 47 | pub const MIN_GAMMA: f64 = 0.1; 48 | pub const MAX_GAMMA: f64 = 10.0; 49 | pub const MIN_LATITUDE: f64 = -90.0; 50 | pub const MAX_LATITUDE: f64 = 90.0; 51 | pub const MIN_LONGITUDE: f64 = -180.0; 52 | pub const MAX_LONGITUDE: f64 = 180.0; 53 | pub const MIN_ELEVATION: f64 = -90.0; 54 | pub const MAX_ELEVATION: f64 = 90.0; 55 | 56 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 57 | pub struct Temperature(u16); 58 | 59 | #[derive(Debug, Clone, Copy)] 60 | pub struct Brightness(f64); 61 | 62 | #[derive(Debug, Clone, Copy)] 63 | pub struct Gamma([f64; 3]); 64 | 65 | #[derive(Debug, Clone, PartialEq, Default)] 66 | pub struct ColorSettings { 67 | pub temp: Temperature, 68 | pub gamma: Gamma, 69 | pub brght: Brightness, 70 | } 71 | 72 | #[derive(Debug, Clone, Copy, Generic)] 73 | pub struct Time { 74 | pub hour: u8, 75 | pub minute: u8, 76 | } 77 | 78 | /// Offset from midnight in seconds 79 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 80 | pub struct TimeOffset(u32); 81 | 82 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 83 | pub struct TimeRange { 84 | pub start: TimeOffset, 85 | pub end: TimeOffset, 86 | } 87 | 88 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Generic)] 89 | pub struct TimeRanges { 90 | pub dawn: TimeRange, 91 | pub dusk: TimeRange, 92 | } 93 | 94 | #[derive(Debug, Clone, Copy, PartialOrd)] 95 | pub struct Elevation(f64); 96 | 97 | /// The solar elevations at which the transition begins/ends, 98 | #[derive(Debug, Clone, Copy, PartialEq)] 99 | pub struct ElevationRange { 100 | pub high: Elevation, 101 | pub low: Elevation, 102 | } 103 | 104 | #[derive(Debug, Clone, Copy)] 105 | pub struct Latitude(f64); 106 | #[derive(Debug, Clone, Copy)] 107 | pub struct Longitude(f64); 108 | #[derive(Debug, Clone, Copy, Default, PartialEq, Generic)] 109 | pub struct Location { 110 | pub lat: Latitude, 111 | pub lon: Longitude, 112 | } 113 | 114 | #[derive(Debug, Clone, Copy, Default, PartialEq)] 115 | pub enum Mode { 116 | #[default] 117 | Daemon, 118 | Oneshot, 119 | Set, 120 | Reset, 121 | Print, 122 | } 123 | 124 | #[derive(Debug, Clone, PartialEq)] 125 | pub enum TransitionScheme { 126 | Time(TimeRanges), 127 | Elev(ElevationRange), 128 | } 129 | 130 | #[derive(Debug, Clone, PartialEq)] 131 | pub enum LocationProviderType { 132 | Manual(Location), 133 | Geoclue2, 134 | } 135 | 136 | #[derive(Debug, Clone, PartialEq, Eq)] 137 | pub enum AdjustmentMethodType { 138 | Dummy, 139 | 140 | #[cfg(unix_without_macos)] 141 | Drm { 142 | card_num: Option, 143 | crtcs: Vec, 144 | }, 145 | 146 | #[cfg(unix_without_macos)] 147 | Randr { 148 | screen_num: Option, 149 | crtcs: Vec, 150 | }, 151 | 152 | #[cfg(unix_without_macos)] 153 | Vidmode { 154 | screen_num: Option, 155 | }, 156 | 157 | #[cfg(windows)] 158 | Win32Gdi, 159 | } 160 | 161 | #[derive(Debug, Clone, Copy)] 162 | pub struct Alpha(f64); 163 | 164 | #[derive(Debug, Clone, Copy, PartialEq)] 165 | pub enum Period { 166 | Daytime, 167 | Night, 168 | Transition { 169 | progress: u8, // Between 0 and 100 170 | }, 171 | } 172 | 173 | #[derive(Debug, Clone, Generic)] 174 | pub struct DayNight { 175 | pub day: T, 176 | pub night: T, 177 | } 178 | 179 | pub type TemperatureRange = DayNight; 180 | pub type BrightnessRange = DayNight; 181 | pub type GammaRange = DayNight; 182 | 183 | // 184 | 185 | impl Default for Temperature { 186 | fn default() -> Self { 187 | Self(DEFAULT_TEMPERATURE) 188 | } 189 | } 190 | 191 | impl Default for Brightness { 192 | fn default() -> Self { 193 | Self(DEFAULT_BRIGHTNESS) 194 | } 195 | } 196 | 197 | impl Default for Gamma { 198 | fn default() -> Self { 199 | Self([DEFAULT_GAMMA; 3]) 200 | } 201 | } 202 | 203 | impl Default for Elevation { 204 | fn default() -> Self { 205 | Self(0.0) 206 | } 207 | } 208 | 209 | impl Default for ElevationRange { 210 | fn default() -> Self { 211 | Self { 212 | high: Elevation(DEFAULT_ELEVATION_HIGH), 213 | low: Elevation(DEFAULT_ELEVATION_LOW), 214 | } 215 | } 216 | } 217 | 218 | impl Default for Latitude { 219 | fn default() -> Self { 220 | Self(DEFAULT_LATITUDE) 221 | } 222 | } 223 | 224 | impl Default for Longitude { 225 | fn default() -> Self { 226 | Self(DEFAULT_LONGITUDE) 227 | } 228 | } 229 | 230 | impl ColorSettings { 231 | pub fn default_day() -> Self { 232 | Self { 233 | temp: Temperature(DEFAULT_TEMPERATURE_DAY), 234 | ..Default::default() 235 | } 236 | } 237 | 238 | pub fn default_night() -> Self { 239 | Self { 240 | temp: Temperature(DEFAULT_TEMPERATURE_NIGHT), 241 | ..Default::default() 242 | } 243 | } 244 | } 245 | 246 | impl Default for TransitionScheme { 247 | fn default() -> Self { 248 | Self::Elev(Default::default()) 249 | } 250 | } 251 | 252 | impl Default for LocationProviderType { 253 | fn default() -> Self { 254 | Self::Manual(Default::default()) 255 | } 256 | } 257 | 258 | impl Default for Period { 259 | fn default() -> Self { 260 | Self::Daytime 261 | } 262 | } 263 | 264 | // 265 | 266 | pub fn gamma(n: f64) -> Result { 267 | if (MIN_GAMMA..=MAX_GAMMA).contains(&n) { 268 | Ok(n) 269 | } else { 270 | Err(GammaError(n)) 271 | } 272 | } 273 | 274 | impl TryFrom for Temperature { 275 | type Error = TemperatureError; 276 | 277 | fn try_from(n: u16) -> Result { 278 | if (MIN_TEMPERATURE..=MAX_TEMPERATURE).contains(&n) { 279 | Ok(Self(n)) 280 | } else { 281 | Err(TemperatureError(n)) 282 | } 283 | } 284 | } 285 | 286 | impl TryFrom for Brightness { 287 | type Error = BrightnessError; 288 | 289 | fn try_from(n: f64) -> Result { 290 | if (MIN_BRIGHTNESS..=MAX_BRIGHTNESS).contains(&n) { 291 | Ok(Self(n)) 292 | } else { 293 | Err(BrightnessError(n)) 294 | } 295 | } 296 | } 297 | 298 | impl TryFrom for Gamma { 299 | type Error = GammaError; 300 | 301 | fn try_from(n: f64) -> Result { 302 | Ok(Self([gamma(n)?; 3])) 303 | } 304 | } 305 | 306 | impl TryFrom<(f64, f64, f64)> for Gamma { 307 | type Error = GammaRgbError; 308 | 309 | fn try_from((r, g, b): (f64, f64, f64)) -> Result { 310 | let (r, g, b) = (gamma(r).into_validated() + gamma(g) + gamma(b)) 311 | .into_result()? 312 | .into_generic(); 313 | Ok(Self([r, g, b])) 314 | } 315 | } 316 | 317 | impl TryFrom for Latitude { 318 | type Error = LatitudeError; 319 | 320 | fn try_from(n: f64) -> Result { 321 | if (MIN_LATITUDE..=MAX_LATITUDE).contains(&n) { 322 | Ok(Self(n)) 323 | } else { 324 | Err(LatitudeError(n)) 325 | } 326 | } 327 | } 328 | 329 | impl TryFrom for Longitude { 330 | type Error = LongitudeError; 331 | 332 | fn try_from(n: f64) -> Result { 333 | if (MIN_LONGITUDE..=MAX_LONGITUDE).contains(&n) { 334 | Ok(Self(n)) 335 | } else { 336 | Err(LongitudeError(n)) 337 | } 338 | } 339 | } 340 | 341 | impl TryFrom for Elevation { 342 | type Error = ElevationError; 343 | 344 | fn try_from(n: f64) -> Result { 345 | if (MIN_ELEVATION..=MAX_ELEVATION).contains(&n) { 346 | Ok(Self(n)) 347 | } else { 348 | Err(ElevationError(n)) 349 | } 350 | } 351 | } 352 | 353 | impl TryFrom<(f64, f64)> for Location { 354 | type Error = LocationError; 355 | 356 | fn try_from((lat, lon): (f64, f64)) -> Result { 357 | Ok((lat.try_into().inject_err().into_validated() 358 | + lon.try_into().inject_err()) 359 | .into_result()? 360 | .into_generic()) 361 | } 362 | } 363 | 364 | pub fn hour(h: u8) -> Result { 365 | if h < 24 { 366 | Ok(h) 367 | } else { 368 | Err(HourError(h)) 369 | } 370 | } 371 | 372 | pub fn minute(m: u8) -> Result { 373 | if m < 24 { 374 | Ok(m) 375 | } else { 376 | Err(MinuteError(m)) 377 | } 378 | } 379 | 380 | impl TryFrom<(u8, u8)> for Time { 381 | type Error = TimeError; 382 | 383 | fn try_from((h, m): (u8, u8)) -> Result { 384 | let time = (hour(h).inject_err().into_validated() 385 | + minute(m).inject_err()) 386 | .into_result()? 387 | .into_generic(); 388 | Ok(time) 389 | } 390 | } 391 | 392 | impl From