commit c5dc769bda8da1f07d973824fee4f313f2b72c03 Author: mark <> Date: Sat May 9 19:04:30 2026 +0200 feat: rgbcRGBC, commit, hold diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c016be --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# wlrcolormess + +mess with your output colors using [`wlr-gamma-control-unstable-v1`](https://gitlab.freedesktop.org/wlroots/wlr-protocols/-/blob/master/unstable/wlr-gamma-control-unstable-v1.xml). + +if you (just) want to change color temperature or gamma, you probably want [gammastep](https://gitlab.com/chinstrap/gammastep) instead. + +wlrcolormess reads input lines from its arguments and then from stdin. + +```sh +alias wlrcolormess='python3 /path/to/wlrcolormess/main.py' +``` + +--- + +```sh +wlrcolormess 'c 0 0.2 1' +``` + +Similar to increasing gamma, this maps + +- the range `[0, 0.5]` to `[0, 0.2]` (less darker colors) +- the range `[0.5, 1]` to `[0.2, 1]` (more brighter colors) + +--- + +```sh +wlrcolormess 'c 0 0.5:0.2 1' +``` + +Similar to decreasing gamma, this maps + +- the range `[0, 0.2]` to `[0, 0.5]` (more darker colors) +- the range `[0.2, 1]` to `[0.5, 1]` (less brighter colors) + +--- + +```sh +wlrcolormess 'c .2 .0:.2 .4:.2 .2:.4 .6:.4 .4:.6 .8:.6 .6:.8 1:.8 .8' +``` + +finally: little-endian colors. diff --git a/main.py b/main.py new file mode 100644 index 0000000..b0d2d14 --- /dev/null +++ b/main.py @@ -0,0 +1,203 @@ +# adapted from +# https://python-wayland.org/examples/ +# examples/20-list-monitors.py +# uses zwlr_gamma_control_manager_v1 +# https://python-wayland.org/wayland/zwlr_gamma_control_manager_v1/ + +# default gamma ramps +r = [0, 1] +g = [0, 1] +b = [0, 1] + +import os, sys +from time import sleep +import wayland +from wayland.client import wayland_class +from itertools import chain + +@wayland_class("wl_registry") +class Registry(wayland.wl_registry): + def __init__(self): + super().__init__() + self.gamma = None + self.outputs = [] + self.gammas = [] + def on_global(self, name, interface, version): + match interface: + case "zwlr_gamma_control_manager_v1": + self.gamma = self.bind(name, interface, version) + self.gammas = [self.gamma.get_gamma_control(output) for output in self.outputs] + case "wl_output": + output = self.bind(name, interface, version) + self.outputs.append(output) + if self.gamma is not None: + self.gammas.append(self.gamma.get_gamma_control(output)) + +@wayland_class("wl_output") +class Output(wayland.wl_output): + def __init__(self): + super().__init__() + self.done = False + def on_done(self): + self.done = True + +@wayland_class("zwlr_gamma_control_v1") +class GammaControl(wayland.zwlr_gamma_control_v1): + def __init__(self): + super().__init__() + self.done = False + self.size = None + def on_gamma_size(self, size): + self.size = size + self.done = True + def on_failed(self): + self.done = True + +display = wayland.wl_display() +registry = display.get_registry() + +while not registry.outputs or not all(v.done for v in chain(registry.outputs, registry.gammas)): + display.dispatch_timeout(0.1) + +def lerp(a, b, f): + return a + f * (b - a) +def scale_ramp(ramp, size): + if size < 2 or len(ramp) < 2: + raise ValueError() + ranges = len(ramp) - 1 + end = 0 + curr = ramp[0] + if isinstance(curr, tuple): + a, b = curr + curr = a + for i in range(1, len(ramp)): + prev = curr + start = end + if isinstance(ramp[i], tuple): + a, b = ramp[i] + curr = a + end = int((size-1) * b) + else: + curr = ramp[i] + end = (size-1) * i // ranges + width = end - start + if width <= 0: + end = start + else: + for j in range(0, width): + # linearly interpolate and convert to u16 + n = int(lerp(prev, curr, j / width) * 65535) + yield n & 0xFF + yield (n >> 8) & 0xFF + if isinstance(ramp[-1], tuple): + a, b = ramp[-1] + n = int(a * 65535) + else: + n = int(ramp[-1] * 65535) + yield n & 0xFF + yield (n >> 8) & 0xFF + +def parse_ramp(line, prev): + line = line.strip() + if line: + return [float(f) if not ':' in f else (float(f.split(':')[0]), float(f.split(':')[1])) for f in line.split()] + else: + out = "" + for f in prev: + if out: + out += ' ' + if isinstance(f, tuple): + a, b = f + out += str(a)+':'+str(b) + else: + out += str(f) + print(out) + return prev +def parse_ramp_int(line, prev): + line = line.strip() + if line: + return [float(f) / 255 if not ':' in f else (float(f.split(':')[0]) / 255, float(f.split(':')[1]) / 255) for f in line.split()] + else: + out = "" + for f in prev: + if out: + out += ' ' + if isinstance(f, tuple): + a, b = f + out += str(int(round(a * 255)))+':'+str(int(round(b * 255))) + else: + out += str(int(round(f * 255))) + print(out) + return prev + +def execute(line): + global r, g, b + match line.split()[0]: + case "r": + r = parse_ramp(line[1:].strip(), r) + case "g": + g = parse_ramp(line[1:].strip(), g) + case "b": + b = parse_ramp(line[1:].strip(), b) + case "c": + r = parse_ramp(line[1:].strip(), [0, 1]) + g = r + b = r + case "R": + r = parse_ramp_int(line[1:].strip(), r) + case "G": + g = parse_ramp_int(line[1:].strip(), g) + case "B": + b = parse_ramp_int(line[1:].strip(), b) + case "C": + r = parse_ramp_int(line[1:].strip(), [0, 1]) + g = r + b = r + case "commit": + for i, gamma in enumerate(registry.gammas): + if gamma.size: + fd = os.memfd_create(f"gamma{i}", os.MFD_CLOEXEC) + os.lseek(fd, 0, os.SEEK_SET) + data = bytearray((by for ramp in [r, g, b] for by in scale_ramp(ramp, gamma.size))) + os.write(fd, data) + os.truncate(fd, 3*2*gamma.size) + os.lseek(fd, 0, os.SEEK_SET) + gamma.set_gamma(fd) + case "hold": + execute("commit") + try: + while True: + sleep(60) + except: + exit() + +for arg in sys.argv[1:]: + if arg.startswith("-"): + print(""" +wlrcolormess expects each argument or line from stdin to be one of the following: + +commit - apply color ramps (automatically done after argument parsing) +hold - commit, then close stdin, but don't exit +[rgbRGB] - print current color ramps +[cC] - reset color ramps to default and print one +[rgbc] - set ramp for one or all colors, range 0-1 +[RGBC] - set ramp for one or all colors, range 0-255 + +: a whitespace-separated list of color values: + `0 1`, `0 0.2 1`, `0 0.5:0.2 1` for [rgbc]. + `0 255`, `0 50 255`, `0 127.5:50 255` for [RGBC]. + values larger than 1 or 255 will overflow. +""") + exit() + execute(arg) +execute("commit") + +while True: + try: + line = input() + except: + break + try: + execute(line) + except: + print("error")