#!/usr/bin/python3 # Licensed under the GNU GPLv3+ # Authors: nicholasbishop, bgstack15 # 2022-09-26-2 10:56 added basic 2-monitor support. import math, os, ast import subprocess import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk def xrandr(args): cmd = ['xrandr'] + args return subprocess.check_output(cmd).decode('utf-8') def get_connected_outputs(): for line in xrandr([]).splitlines(): if line.startswith(' '): continue words = line.split() if words[1] == 'connected': yield words[0] def set_brightness_and_gamma(outputs, brightness, gamma): #print(f"DEBUG (sbag): {outputs}, {brightness}, {gamma}") args = [] x = 0 while x < len(outputs): args += ['--output', outputs[x], '--brightness', str(brightness[x]), '--gamma', '{}:{}:{}'.format(gamma[x][0], gamma[x][1], gamma[x][2])] x = x + 1 print(' '.join(args)) xrandr(args) def color_temperature_to_rgb(kelvin): """Adapted from tannerhelland.com/4435.""" temp = kelvin / 100.0 if temp <= 66: red = 255 green = temp green = 99.4708025861 * math.log(green) - 161.1195681661 if temp <= 19: blue = 0 else: blue = temp-10 blue = 138.5177312231 * math.log(blue) - 305.0447927307 else: red = temp - 60 red = 329.698727446 * math.pow(red, -0.1332047592) green = temp - 60 green = 288.1221695283 * math.pow(green, -0.0755148492) blue = 255 def clamp(val): if val < 0: return 0 elif val > 255: return 255 else: return val return [clamp(red), clamp(green), clamp(blue)] def temperature_to_gamma(kelvin): rgb = color_temperature_to_rgb(kelvin) fac = sum(rgb) / 3 for i in range(3): rgb[i] /= (fac) return rgb class MyWindow(Gtk.Window): def __init__(self, conf): Gtk.Window.__init__(self, title="xrandr-slightly-fewer-tears") self.set_default_size(640, -1) self.set_default_icon_name("display") self.conf = conf self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, border_width=10) self.add(self.box) _ = get_connected_outputs() self.outputs = [] for i in _: self.outputs.append(i) #print(f"outputs = {self.outputs}") #print(f"len(outputs) = {len(self.outputs)}") x = 0 self.brightness_checkbutton = [] self.brightness = [] self.temperature_checkbutton = [] self.temperature = [] while x < len(self.outputs): self.brightness_checkbutton.append(self.add_checkbutton( f"Brightness {self.outputs[x]}", conf.brightness_enabled[x])) self.brightness.append(self.add_hscale(20, 100, conf.brightness[x])) self.temperature_checkbutton.append(self.add_checkbutton( 'Temperature (K)', False)) self.temperature.append(self.add_hscale(2000, 10000, conf.temperature[x])) print(x) x = x + 1 # Update immediately so that saved values are loaded on startup self.update() def add_checkbutton(self, label, default): btn = Gtk.CheckButton(label) btn.set_active(default) btn.connect('toggled', lambda _: self.update()) self.box.add(btn) return btn def add_hscale(self, min_val, max_val, def_val): scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, digits=0) scale.set_range(min_val, max_val) scale.set_value(def_val) scale.connect('value-changed', lambda _: self.update()) self.box.add(scale) return scale def save_conf(self): self.conf.brightness_enabled = [] self.conf.temperature_enabled = [] self.conf.brightness = [] self.conf.temperature = [] x = 0 while x < len(self.outputs): self.conf.brightness_enabled.append(self.brightness_checkbutton[x].get_active()) self.conf.temperature_enabled.append(self.temperature_checkbutton[x].get_active()) self.conf.brightness.append(self.brightness[x].get_value()) self.conf.temperature.append(self.temperature[x].get_value()) x = x + 1 self.conf.save() def update(self): x = 0 brightness = [] gamma = [] while x < len(self.outputs): brightness.append(1) if self.brightness_checkbutton[x].get_active(): brightness[x] = self.brightness[x].get_value() / 100.0 if brightness[x] < 0.2: brightness[x] = 0.2 else: brightness[x] = 1.0 gamma.append((1,1,1)) if self.temperature_checkbutton[x].get_active(): gamma[x] = temperature_to_gamma(self.temperature[x].get_value()) else: gamma[x] = (1, 1, 1) x = x + 1 set_brightness_and_gamma(self.outputs, brightness, gamma) self.save_conf() class Config(object): def __init__(self): self.pardir = os.path.join(os.getenv('HOME'), '.config', 'xrandr-slightly-fewer-tears') self.path = os.path.join(self.pardir, 'xsft.conf') # key:default self.items = { 'brightness': [80.0,80.0], 'brightness_enabled': [True, True], 'temperature': [5500.0, 5500.0], 'temperature_enabled': [True, True] } # Set defaults for key in self.items: setattr(self, key, self.items[key]) try: self.load() except IOError: pass def load(self): with open(self.path) as conf_file: for line in conf_file.readlines(): parts = line.split('=') key = parts[0] val_str = ast.literal_eval(parts[1]) default = self.items.get(key) #print(f"Default = {default}, val_str={val_str} {type(val_str)}") if default is not None: if isinstance(default[0], bool): val = [bool(x) for x in val_str] else: val = [float(x) for x in val_str] setattr(self, key, val) def save(self): if not os.path.isdir(self.pardir): os.mkdir(self.pardir) with open(self.path, 'w') as conf_file: for key in self.items: val = getattr(self, key) #print('DEBUG(save): {}={}\n'.format(key, val)) conf_file.write('{}={}\n'.format(key, val)) def main(): conf = Config() win = MyWindow(conf) win.connect("delete-event", Gtk.main_quit) win.show_all() Gtk.main() if __name__ == '__main__': main()