├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── data └── system76-oled.desktop ├── debian ├── changelog ├── compat ├── control ├── copyright ├── rules └── source │ └── format ├── rust-toolchain └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo 2 | /target 3 | /vendor 4 | /vendor.tar.xz 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.18" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "atty" 14 | version = "0.2.14" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 17 | dependencies = [ 18 | "hermit-abi", 19 | "libc", 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "1.3.2" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 28 | 29 | [[package]] 30 | name = "cfg-if" 31 | version = "1.0.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 34 | 35 | [[package]] 36 | name = "dbus" 37 | version = "0.9.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" 40 | dependencies = [ 41 | "libc", 42 | "libdbus-sys", 43 | "winapi", 44 | ] 45 | 46 | [[package]] 47 | name = "env_logger" 48 | version = "0.9.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" 51 | dependencies = [ 52 | "atty", 53 | "humantime", 54 | "log", 55 | "regex", 56 | "termcolor", 57 | ] 58 | 59 | [[package]] 60 | name = "hermit-abi" 61 | version = "0.1.19" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 64 | dependencies = [ 65 | "libc", 66 | ] 67 | 68 | [[package]] 69 | name = "humantime" 70 | version = "2.1.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 73 | 74 | [[package]] 75 | name = "inotify" 76 | version = "0.10.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" 79 | dependencies = [ 80 | "bitflags", 81 | "inotify-sys", 82 | "libc", 83 | ] 84 | 85 | [[package]] 86 | name = "inotify-sys" 87 | version = "0.1.5" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 90 | dependencies = [ 91 | "libc", 92 | ] 93 | 94 | [[package]] 95 | name = "libc" 96 | version = "0.2.131" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40" 99 | 100 | [[package]] 101 | name = "libdbus-sys" 102 | version = "0.2.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" 105 | dependencies = [ 106 | "pkg-config", 107 | ] 108 | 109 | [[package]] 110 | name = "log" 111 | version = "0.4.17" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 114 | dependencies = [ 115 | "cfg-if", 116 | ] 117 | 118 | [[package]] 119 | name = "memchr" 120 | version = "2.5.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 123 | 124 | [[package]] 125 | name = "pkg-config" 126 | version = "0.3.25" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 129 | 130 | [[package]] 131 | name = "regex" 132 | version = "1.6.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 135 | dependencies = [ 136 | "aho-corasick", 137 | "memchr", 138 | "regex-syntax", 139 | ] 140 | 141 | [[package]] 142 | name = "regex-syntax" 143 | version = "0.6.27" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 146 | 147 | [[package]] 148 | name = "system76-oled" 149 | version = "0.1.4" 150 | dependencies = [ 151 | "dbus", 152 | "env_logger", 153 | "inotify", 154 | "libc", 155 | "log", 156 | "x11", 157 | ] 158 | 159 | [[package]] 160 | name = "termcolor" 161 | version = "1.1.3" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 164 | dependencies = [ 165 | "winapi-util", 166 | ] 167 | 168 | [[package]] 169 | name = "winapi" 170 | version = "0.3.9" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 173 | dependencies = [ 174 | "winapi-i686-pc-windows-gnu", 175 | "winapi-x86_64-pc-windows-gnu", 176 | ] 177 | 178 | [[package]] 179 | name = "winapi-i686-pc-windows-gnu" 180 | version = "0.4.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 183 | 184 | [[package]] 185 | name = "winapi-util" 186 | version = "0.1.5" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 189 | dependencies = [ 190 | "winapi", 191 | ] 192 | 193 | [[package]] 194 | name = "winapi-x86_64-pc-windows-gnu" 195 | version = "0.4.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 198 | 199 | [[package]] 200 | name = "x11" 201 | version = "2.20.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" 204 | dependencies = [ 205 | "libc", 206 | "pkg-config", 207 | ] 208 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "system76-oled" 3 | description = "Control brightness on OLED displays" 4 | version = "0.1.4" 5 | repository = "https://github.com/pop-os/system76-oled" 6 | license = "LGPL-3.0-or-later" 7 | authors = ["Jeremy Soller "] 8 | edition = "2018" 9 | 10 | [dependencies] 11 | dbus = "0.9.6" 12 | env_logger = "0.9.0" 13 | inotify = { version = "0.10.0", default-features = false } 14 | libc = "0.2.131" 15 | log = { version = "0.4.17", features = ["release_max_level_debug"] } 16 | x11 = { version = "2.20.0", features = ["xlib", "xrandr"] } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prefix ?= /usr 2 | sysconfdir ?= /etc 3 | exec_prefix = $(prefix) 4 | bindir = $(exec_prefix)/bin 5 | libdir = $(exec_prefix)/lib 6 | includedir = $(prefix)/include 7 | datarootdir = $(prefix)/share 8 | datadir = $(datarootdir) 9 | 10 | SRC = Cargo.toml Cargo.lock Makefile $(shell find src -type f -wholename '*src/*.rs') 11 | 12 | .PHONY: all clean distclean install uninstall update 13 | 14 | BIN=system76-oled 15 | 16 | DEBUG ?= 0 17 | ifeq ($(DEBUG),0) 18 | ARGS += "--release" 19 | TARGET = release 20 | endif 21 | 22 | VENDORED ?= 0 23 | ifeq ($(VENDORED),1) 24 | ARGS += "--frozen" 25 | endif 26 | 27 | all: target/release/$(BIN) 28 | 29 | clean: 30 | cargo clean 31 | 32 | distclean: 33 | rm -rf .cargo vendor vendor.tar.xz 34 | 35 | install: all 36 | install -D -m 04755 "target/release/$(BIN)" "$(DESTDIR)$(bindir)/$(BIN)" 37 | install -D -m 0644 "data/$(BIN).desktop" "$(DESTDIR)$(sysconfdir)/xdg/autostart/$(BIN).desktop" 38 | 39 | uninstall: 40 | rm -f "$(DESTDIR)$(bindir)/$(BIN)" 41 | rm -f "$(DESTDIR)$(sysconfdir)/xdg/autostart/$(BIN).desktop" 42 | 43 | update: 44 | cargo update 45 | 46 | vendor: 47 | mkdir -p .cargo 48 | cargo vendor | head -n -1 > .cargo/config 49 | echo 'directory = "vendor"' >> .cargo/config 50 | tar pcfJ vendor.tar.xz vendor 51 | rm -rf vendor 52 | 53 | target/release/$(BIN): $(SRC) 54 | ifeq ($(VENDORED),1) 55 | tar pxf vendor.tar.xz 56 | endif 57 | cargo build $(ARGS) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # system76-oled 2 | Control brightness on OLED displays 3 | -------------------------------------------------------------------------------- /data/system76-oled.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name=System76 OLED 5 | Comment=Control brightness on OLED displays 6 | Exec=system76-oled 7 | Terminal=false 8 | NoDisplay=true 9 | StartupNotify=false 10 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | system76-oled (0.1.5) focal; urgency=medium 2 | 3 | * Add oryp10 4 | 5 | -- Tim Crawford Mon, 22 Aug 2022 10:42:51 -0600 6 | 7 | system76-oled (0.1.4) bionic; urgency=medium 8 | 9 | * Add addw2 10 | 11 | -- Jeremy Soller Wed, 22 Apr 2020 21:09:40 -0600 12 | 13 | system76-oled (0.1.3) bionic; urgency=medium 14 | 15 | * Switch to using xlib for performance 16 | * Reapply gamma when outputs change 17 | * Work around mutter overriding gamma 18 | 19 | -- Jeremy Soller Thu, 18 Jul 2019 10:37:36 -0600 20 | 21 | system76-oled (0.1.2) bionic; urgency=medium 22 | 23 | * Add timeout of one second to ensure sync on resuming from suspend 24 | 25 | -- Jeremy Soller Thu, 18 Jul 2019 08:16:00 -0600 26 | 27 | system76-oled (0.1.1) bionic; urgency=medium 28 | 29 | * Update inotify to 0.7.0 30 | 31 | -- Jeremy Soller Wed, 17 Jul 2019 10:28:19 -0600 32 | 33 | system76-oled (0.1.0) bionic; urgency=medium 34 | 35 | * Initial release. 36 | 37 | -- Jeremy Soller Wed, 17 Jul 2019 10:18:55 -0600 38 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: system76-oled 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Jeremy Soller 5 | Build-Depends: 6 | debhelper (>=9), 7 | cargo, 8 | libdbus-1-dev, 9 | libx11-dev, 10 | libxrandr-dev, 11 | pkg-config 12 | Standards-Version: 4.1.1 13 | Homepage: https://github.com/pop-os/system76-oled 14 | 15 | Package: system76-oled 16 | Architecture: amd64 17 | Depends: 18 | ${misc:Depends}, 19 | ${shlib:Depends} 20 | Description: Control brightness on OLED displays 21 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: system76-oled 3 | Source: https://github.com/pop-os/system76-oled 4 | 5 | Files: * 6 | Copyright: Copyright 2018 System76 7 | License: LGPL-3 8 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export VENDORED ?= 1 4 | CLEAN ?= 1 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_auto_build: 10 | env CARGO_HOME="$$(pwd)/target/cargo" \ 11 | dh_auto_build 12 | 13 | override_dh_auto_clean: 14 | ifeq ($(CLEAN),1) 15 | make clean 16 | endif 17 | ifeq ($(VENDORED),1) 18 | if ! ischroot; then \ 19 | make vendor; \ 20 | fi 21 | endif 22 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.58.0 2 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use env_logger::Env; 2 | use inotify::{ 3 | Inotify, 4 | WatchMask, 5 | }; 6 | use log::{debug, error, info, trace}; 7 | use std::{env, fs, mem, process, ptr, slice, str}; 8 | use std::io::{Error, Read, Seek, SeekFrom}; 9 | use std::os::unix::io::{AsRawFd, RawFd}; 10 | use std::ptr::NonNull; 11 | use x11::{xlib, xrandr}; 12 | 13 | pub struct ScreenNumber(libc::c_int); 14 | 15 | pub struct RootWindow(libc::c_ulong); 16 | 17 | pub struct Crtc(xrandr::RRCrtc); 18 | 19 | pub struct CrtcGamma(NonNull); 20 | 21 | impl CrtcGamma { 22 | pub fn size(&self) -> libc::c_int { 23 | unsafe { 24 | self.0.as_ref().size 25 | } 26 | } 27 | 28 | pub fn channels(&mut self) -> (&mut [libc::c_ushort], &mut [libc::c_ushort], &mut [libc::c_ushort]) { 29 | unsafe { 30 | ( 31 | slice::from_raw_parts_mut( 32 | self.0.as_ref().red, 33 | self.0.as_ref().size as usize 34 | ), 35 | slice::from_raw_parts_mut( 36 | self.0.as_ref().green, 37 | self.0.as_ref().size as usize 38 | ), 39 | slice::from_raw_parts_mut( 40 | self.0.as_ref().blue, 41 | self.0.as_ref().size as usize 42 | ), 43 | ) 44 | } 45 | } 46 | } 47 | 48 | impl Drop for CrtcGamma { 49 | fn drop(&mut self) { 50 | unsafe { 51 | xrandr::XRRFreeGamma(self.0.as_ptr()); 52 | } 53 | } 54 | } 55 | 56 | pub struct OutputInfo(NonNull); 57 | 58 | impl OutputInfo { 59 | pub fn name(&self) -> &[u8] { 60 | unsafe { 61 | slice::from_raw_parts( 62 | self.0.as_ref().name as *const u8, 63 | self.0.as_ref().nameLen as usize 64 | ) 65 | } 66 | } 67 | 68 | pub fn crtc(&self) -> Option { 69 | let crtc = unsafe { 70 | self.0.as_ref().crtc 71 | }; 72 | if crtc == 0 { 73 | None 74 | } else { 75 | Some(Crtc(crtc)) 76 | } 77 | } 78 | } 79 | 80 | impl Drop for OutputInfo { 81 | fn drop(&mut self) { 82 | unsafe { 83 | xrandr::XRRFreeOutputInfo(self.0.as_ptr()); 84 | } 85 | } 86 | } 87 | 88 | pub struct Output(xrandr::RROutput); 89 | 90 | pub struct OutputsIter<'a> { 91 | items: &'a [xrandr::RROutput], 92 | i: usize, 93 | } 94 | 95 | impl<'a> Iterator for OutputsIter<'a> { 96 | type Item = Output; 97 | fn next(&mut self) -> Option { 98 | if let Some(item) = self.items.get(self.i) { 99 | self.i += 1; 100 | Some(Output(*item)) 101 | } else { 102 | None 103 | } 104 | } 105 | } 106 | 107 | pub struct ScreenResources(NonNull); 108 | 109 | impl ScreenResources { 110 | pub fn outputs(&self) -> OutputsIter { 111 | let items = unsafe { 112 | slice::from_raw_parts( 113 | self.0.as_ref().outputs, 114 | self.0.as_ref().noutput as usize 115 | ) 116 | }; 117 | OutputsIter { 118 | items, 119 | i: 0, 120 | } 121 | } 122 | } 123 | 124 | impl Drop for ScreenResources { 125 | fn drop(&mut self) { 126 | unsafe { 127 | xrandr::XRRFreeScreenResources(self.0.as_ptr()); 128 | } 129 | } 130 | } 131 | 132 | pub struct Display(NonNull); 133 | 134 | impl Display { 135 | pub fn new() -> Option { 136 | NonNull::new(unsafe { 137 | xlib::XOpenDisplay(ptr::null()) 138 | }).map(Self) 139 | } 140 | 141 | pub fn default_screen_number(&self) -> ScreenNumber { 142 | ScreenNumber(unsafe { 143 | xlib::XDefaultScreen(self.0.as_ptr()) 144 | }) 145 | } 146 | 147 | pub fn root_window(&self, screen_number: &ScreenNumber) -> RootWindow { 148 | RootWindow(unsafe { 149 | xlib::XRootWindow(self.0.as_ptr(), screen_number.0) 150 | }) 151 | } 152 | 153 | pub fn get_screen_resources(&self, root_window: &RootWindow, current: bool) -> Option { 154 | NonNull::new(unsafe { 155 | if current { 156 | xrandr::XRRGetScreenResourcesCurrent(self.0.as_ptr(), root_window.0) 157 | } else { 158 | xrandr::XRRGetScreenResources(self.0.as_ptr(), root_window.0) 159 | } 160 | }).map(ScreenResources) 161 | } 162 | 163 | pub fn get_output_info(&self, resources: &ScreenResources, output: &Output) -> Option { 164 | NonNull::new(unsafe { 165 | xrandr::XRRGetOutputInfo(self.0.as_ptr(), resources.0.as_ptr(), output.0) 166 | }).map(OutputInfo) 167 | } 168 | 169 | pub fn get_crtc_gamma(&self, crtc: &Crtc) -> Option { 170 | NonNull::new(unsafe { 171 | xrandr::XRRGetCrtcGamma(self.0.as_ptr(), crtc.0) 172 | }).map(CrtcGamma) 173 | } 174 | 175 | pub fn set_crtc_gamma(&mut self, crtc: &Crtc, gamma: &CrtcGamma) { 176 | unsafe { 177 | xrandr::XRRSetCrtcGamma(self.0.as_ptr(), crtc.0, gamma.0.as_ptr()); 178 | } 179 | } 180 | 181 | pub fn select_input(&mut self, root_window: &RootWindow, mask: libc::c_int) { 182 | unsafe { 183 | xrandr::XRRSelectInput(self.0.as_ptr(), root_window.0, mask); 184 | } 185 | } 186 | 187 | pub fn flush(&mut self) { 188 | unsafe { 189 | xlib::XFlush(self.0.as_ptr()); 190 | } 191 | } 192 | 193 | pub fn pending(&self) -> libc::c_int { 194 | unsafe { 195 | xlib::XPending(self.0.as_ptr()) 196 | } 197 | } 198 | } 199 | 200 | impl AsRawFd for Display { 201 | fn as_raw_fd(&self) -> RawFd { 202 | unsafe { 203 | xlib::XConnectionNumber(self.0.as_ptr()) 204 | } 205 | } 206 | } 207 | 208 | impl Drop for Display { 209 | fn drop(&mut self) { 210 | unsafe { 211 | xlib::XCloseDisplay(self.0.as_ptr()); 212 | } 213 | } 214 | } 215 | 216 | fn xrandr_output_brightness(display: &mut Display, root_window: &RootWindow, output_name: &str, brightness_opt: Option) { 217 | if let Some(resources) = display.get_screen_resources(&root_window, true) { 218 | trace!("resources {:p}", resources.0.as_ptr()); 219 | for output in resources.outputs() { 220 | trace!("output {:#x}", output.0); 221 | if let Some(info) = display.get_output_info(&resources, &output) { 222 | trace!("info {:p}", info.0.as_ptr()); 223 | if let Ok(name) = str::from_utf8(info.name()) { 224 | trace!("name {}", name); 225 | if name.starts_with(output_name) { 226 | trace!("matches {}", output_name); 227 | if let Some(crtc) = info.crtc() { 228 | trace!("crtc {:#x}", crtc.0); 229 | if let Some(mut gamma) = display.get_crtc_gamma(&crtc) { 230 | trace!("gamma {:p}", gamma.0.as_ptr()); 231 | 232 | let size = gamma.size() as usize; 233 | let (red, green, blue) = gamma.channels(); 234 | for i in 0..size { 235 | let r = &mut red[i]; 236 | let g = &mut green[i]; 237 | let b = &mut blue[i]; 238 | 239 | let calulate_value = |gamma_opt: Option| -> u16 { 240 | // Calculate standard gamma value 241 | let mut value = (i as f64) / ((size - 1) as f64); 242 | 243 | // Apply gamma for channel 244 | if let Some(gamma) = gamma_opt { 245 | value = value.powf(1.0 / gamma); 246 | } 247 | 248 | // Apply brightness 249 | if let Some(brightness) = brightness_opt { 250 | value *= brightness; 251 | } 252 | 253 | // Convert to short 254 | (value.min(1.0) * 65535.0) as u16 255 | }; 256 | 257 | *r = calulate_value(None); 258 | *g = calulate_value(None); 259 | *b = calulate_value(None); 260 | } 261 | 262 | trace!("set gamma"); 263 | display.set_crtc_gamma(&crtc, &gamma); 264 | 265 | trace!("flush"); 266 | display.flush(); 267 | } else { 268 | error!("failed to get X gamma info"); 269 | } 270 | } 271 | } 272 | } 273 | } else { 274 | error!("failed to get X output info"); 275 | } 276 | } 277 | } else { 278 | error!("failed to get X screen resources"); 279 | } 280 | } 281 | 282 | fn main() { 283 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 284 | 285 | let vendor = fs::read_to_string("/sys/class/dmi/id/sys_vendor") 286 | .unwrap_or(String::new()); 287 | let vendor = vendor.trim(); 288 | let model = fs::read_to_string("/sys/class/dmi/id/product_version") 289 | .unwrap_or(String::new()); 290 | let model = model.trim(); 291 | 292 | let output_opt = match (vendor, model) { 293 | ("System76", "addw1") => Some("eDP-1"), 294 | ("System76", "addw2") => Some("eDP-1"), 295 | ("System76", "oryp10") => Some("eDP-1"), 296 | _ => None, 297 | }; 298 | 299 | let output = if let Some(output) = output_opt { 300 | info!("Vendor '{}' Model '{}' has OLED display on '{}'", vendor, model, output); 301 | output 302 | } else { 303 | debug!("Vendor '{}' Model '{}' does not have OLED display", vendor, model); 304 | process::exit(0); 305 | }; 306 | 307 | if env::var("XDG_CURRENT_DESKTOP") == Ok("pop:GNOME".to_string()) { 308 | info!("Pop GNOME session already provides system76-oled"); 309 | process::exit(0); 310 | } 311 | 312 | let mut inotify = Inotify::init() 313 | .expect("failed to initialize inotify"); 314 | 315 | let requested_path = "/sys/class/backlight/intel_backlight/brightness"; 316 | 317 | let requested_watch = inotify.add_watch(requested_path, WatchMask::MODIFY) 318 | .expect("failed to watch requested brightness"); 319 | 320 | let mut requested_file = fs::File::open(requested_path) 321 | .expect("failed to open requested brightness"); 322 | 323 | let max_path = "/sys/class/backlight/intel_backlight/max_brightness"; 324 | 325 | let max_watch = inotify.add_watch(max_path, WatchMask::MODIFY) 326 | .expect("failed to watch max brightness"); 327 | 328 | let mut max_file = fs::File::open(max_path) 329 | .expect("failed to open max brightness"); 330 | 331 | let mut display = Display::new().expect("failed to open X display"); 332 | trace!("display {:p}", display.0.as_ptr()); 333 | 334 | let mut xrr_event_base = 0; 335 | let mut xrr_error_base = 0; 336 | if unsafe { xrandr::XRRQueryExtension(display.0.as_ptr(), &mut xrr_event_base, &mut xrr_error_base) } == 0 { 337 | panic!("Xrandr extension not found"); 338 | } 339 | trace!("xrr_event_base {:#x}, xrr_error_base {:#x}", xrr_event_base, xrr_error_base); 340 | 341 | let screen_number = display.default_screen_number(); 342 | trace!("screen_number {:#x}", screen_number.0); 343 | 344 | let root_window = display.root_window(&screen_number); 345 | trace!("root_window {:#x}", root_window.0); 346 | 347 | display.select_input(&root_window, xrandr::RROutputChangeNotifyMask); 348 | 349 | let dbus_system = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::System) 350 | .expect("failed to connect to D-Bus system bus"); 351 | dbus_system.add_match("interface='org.freedesktop.ColorManager',member='Changed'") 352 | .expect("failed to watch D-Bus signal"); 353 | dbus_system.add_match("interface='org.freedesktop.ColorManager',member='DeviceChanged'") 354 | .expect("failed to watch D-Bus signal"); 355 | dbus_system.add_match("interface='org.freedesktop.ColorManager',member='ProfileChanged'") 356 | .expect("failed to watch D-Bus signal"); 357 | 358 | let dbus_session = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::Session) 359 | .expect("failed to connect to D-Bus session bus"); 360 | dbus_session.add_match("interface='org.gnome.Mutter.DisplayConfig',member='MonitorsChanged'") 361 | .expect("failed to watch D-Bus signal"); 362 | 363 | let mut pollfds = vec![libc::pollfd { 364 | fd: inotify.as_raw_fd(), 365 | events: libc::POLLIN, 366 | revents: 0, 367 | }, libc::pollfd { 368 | fd: display.as_raw_fd(), 369 | events: libc::POLLIN, 370 | revents: 0, 371 | }]; 372 | 373 | let dbus_system_pollfd = pollfds.len(); 374 | for watch in dbus_system.watch_fds() { 375 | pollfds.push(watch.to_pollfd()); 376 | } 377 | 378 | let dbus_session_pollfd = pollfds.len(); 379 | for watch in dbus_session.watch_fds() { 380 | pollfds.push(watch.to_pollfd()); 381 | } 382 | 383 | let mut requested_update = true; 384 | let mut requested_str = String::with_capacity(256); 385 | let mut requested = 0; 386 | let mut max_update = true; 387 | let mut max_str = String::with_capacity(256); 388 | let mut max = 0; 389 | let mut current = !0; 390 | 391 | let mut timeout = -1; 392 | let mut timeout_times = 0; 393 | loop { 394 | if requested_update { 395 | requested_str.clear(); 396 | requested_file.seek(SeekFrom::Start(0)) 397 | .expect("failed to seek requested brightness"); 398 | requested_file.read_to_string(&mut requested_str) 399 | .expect("failed to read requested brightness"); 400 | requested = requested_str.trim().parse::() 401 | .expect("failed to parse requested brightness"); 402 | requested_update = false; 403 | debug!("requested {}", requested); 404 | } 405 | 406 | if max_update { 407 | max_str.clear(); 408 | max_file.seek(SeekFrom::Start(0)) 409 | .expect("failed to seek max brightness"); 410 | max_file.read_to_string(&mut max_str) 411 | .expect("failed to read max brightness"); 412 | max = max_str.trim().parse::() 413 | .expect("failed to parse max brightness"); 414 | max_update = false; 415 | debug!("max {}", max); 416 | } 417 | 418 | while current != requested { 419 | current = requested; 420 | /* Smooth transition (may require use of xlib for performance) 421 | if current == !0 { 422 | current = requested; 423 | } else if current < requested { 424 | current += 1; 425 | } else if current > requested { 426 | current -= 1; 427 | } 428 | */ 429 | 430 | xrandr_output_brightness(&mut display, &root_window, output, if current == max { 431 | None 432 | } else { 433 | let ratio = current as f64 / max as f64; 434 | Some(ratio * 0.6 + 0.4) 435 | }); 436 | 437 | debug!("current {}%", current); 438 | } 439 | 440 | // Use poll to establish a timeout 441 | for pollfd in pollfds.iter_mut() { 442 | pollfd.revents = 0; 443 | } 444 | trace!("poll fds: {}, timeout: {})", pollfds.len(), timeout); 445 | let count = unsafe { 446 | libc::poll(pollfds.as_mut_ptr(), pollfds.len() as libc::nfds_t, timeout) 447 | }; 448 | trace!("poll fds: {} timeout: {} = {}", pollfds.len(), timeout, count); 449 | 450 | if count < 0 { 451 | panic!("failed to poll: {}", Error::last_os_error()); 452 | } else if count == 0 { 453 | // Update from timeout 454 | current = !0; 455 | if timeout_times == 0 { 456 | timeout = -1; 457 | } else { 458 | timeout_times -= 1; 459 | } 460 | } else { 461 | if pollfds[0].revents > 0 { 462 | let mut buffer = [0; 1024]; 463 | let events = inotify.read_events(&mut buffer) 464 | .expect("failed to read events"); 465 | 466 | for event in events { 467 | trace!("event {:?}", event); 468 | if event.wd == requested_watch { 469 | requested_update = true; 470 | } 471 | if event.wd == max_watch { 472 | max_update = true; 473 | } 474 | } 475 | } 476 | 477 | if pollfds[1].revents > 0 { 478 | while display.pending() > 0 { 479 | unsafe { 480 | let mut event = mem::zeroed::(); 481 | xlib::XNextEvent(display.0.as_ptr(), &mut event); 482 | trace!("event {:#x}", event.type_); 483 | if event.type_ >= xrr_event_base { 484 | let xrr_event_type = event.type_ - xrr_event_base; 485 | trace!("xrr_event {:#x}", xrr_event_type); 486 | if xrr_event_type == xrandr::RRNotify { 487 | let notify_event: &xrandr::XRRNotifyEvent = event.as_ref(); 488 | trace!("notify_event {:?}", notify_event); 489 | if notify_event.subtype == xrandr::RRNotify_OutputChange { 490 | let output_change_event: &xrandr::XRROutputChangeNotifyEvent = event.as_ref(); 491 | trace!("output_change_event {:?}", output_change_event); 492 | } 493 | 494 | current = !0; 495 | } 496 | } 497 | } 498 | } 499 | } 500 | 501 | for pollfd in pollfds[dbus_system_pollfd..dbus_session_pollfd].iter() { 502 | if pollfd.revents > 0 { 503 | for item in dbus_system.watch_handle(pollfd.fd, dbus::ffidisp::WatchEvent::from_revents(pollfd.revents)) { 504 | trace!("dbus system item {:?}", item); 505 | 506 | // Mutter displays have changed, force a brightness update. A timeout is 507 | // used because the gamma changes shortly after receiving this signal 508 | // TODO: Figure out how to avoid mutter setting gamma 509 | current = !0; 510 | timeout = 100; 511 | timeout_times = 10; 512 | } 513 | } 514 | } 515 | 516 | for pollfd in pollfds[dbus_session_pollfd..].iter() { 517 | if pollfd.revents > 0 { 518 | for item in dbus_session.watch_handle(pollfd.fd, dbus::ffidisp::WatchEvent::from_revents(pollfd.revents)) { 519 | trace!("dbus session item {:?}", item); 520 | 521 | // Mutter displays have changed, force a brightness update. A timeout is 522 | // used because the gamma changes shortly after receiving this signal 523 | // TODO: Figure out how to avoid mutter setting gamma 524 | current = !0; 525 | timeout = 100; 526 | timeout_times = 10; 527 | } 528 | } 529 | } 530 | } 531 | } 532 | } 533 | --------------------------------------------------------------------------------