diff options
-rw-r--r-- | TODO | 1 | ||||
-rw-r--r-- | examples/textconf | 369 | ||||
-rw-r--r-- | examples/textconf.conf | 63 | ||||
-rw-r--r-- | info/blueshift.texinfo | 19 | ||||
-rw-r--r-- | src/icc.py | 52 |
5 files changed, 502 insertions, 2 deletions
@@ -1,5 +1,4 @@ High priority: - Add example that parses text-based conf file Add support for monitor hotplugging Add models for calculating fade and refresh rate parameters diff --git a/examples/textconf b/examples/textconf new file mode 100644 index 0000000..7dcff6f --- /dev/null +++ b/examples/textconf @@ -0,0 +1,369 @@ +# -*- python -*- + +# This example uses a text based configuration file to make +# it easier for non-programmers to use Blueshift. It is however +# rather limited, the lisp-esque example is a bit more complex +# but do much more. It will # read a file with the same pathname +# just with ‘.conf’ # appended (‘textconf.conf’ in this case.) +# However, if the filename of this file ends with with ‘rc’, +# that part will be removed, for example, if you rename this +# script to ‘~/.blueshiftrc’ it will read ‘~/.blueshift.conf’ +# rather than ‘~/.blueshiftrc.conf’. + + +# Copyright © 2014 Mattias Andrée (maandree@member.fsf.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import subprocess + + +# Get the name of .conf file +conf = '%s.conf' % (config_file[:-2] if config_file.endswith('rc') else config_file) + + +# Read .conf file +with open(conf, 'r') as file: + conf = file.read() + + +# Parse .conf file +sections = {'blueshift' : []} +section = [] +sections['blueshift'].append(section) +for line in conf.split('\n'): + line = line.strip() + if line.startswith('[') and line.startswith(']'): + section_name = line[1 : -1].strip().lower() + if section_name not in sections: + sections[section_name] = [] + section = [] + sections[section_name].append(section) + elif line.startswith(';') or line.startswith('#'): + continue + elif ('=' in line) or (':' in line): + eq = len(line) if '=' not in line else line.find('=') + cl = len(line) if ':' not in line else line.find(':') + eq = min(eq, cl) + section.append((line[:eq].strip().lower(), line[:eq + 1].strip())) + elif len(line.strip()) > 0: + sys.stderr.buffer.write(('Malformated line: %s' % line).encode('utf-8')) +sys.stderr.buffer.flush() + + +# Default values +location = None +adjustment_method_x = ['randr'] +adjustment_method_tty = ['drm'] +points = ['solar', '3', '-6'] + + +# List of adjustments and temporary monitor information +adjustments = [] +monitors = [] +crtc = None +screen = None +card = None + + +def parse_value(value): + ''' + Parse a setting value + + @param value:str The value to parse + @return :(list<str>, bool, bool, bool) The words in the value string, with commands spawned, + and with 'linear', 'cie' and 'default' filtered out, + and their + existance is put as booleans + ''' + def spawn(cmd): + ''' + Spawn an external process and read its output, but only the first line + + @param cmd:str The command to spawn + @return :str? The first line of the command's output, `None` on failure + ''' + proc = subprocess.Popen(['sh', '-c', cmd], stdout = subprocess.PIPE, stderr = sys.stderr) + output = proc.communicate()[0].split('\n')[0] + if (proc.returncode == 0) and (len(output) > 0): + return output + return None + words, buf, cmd = [], '', None + for c in value: + if cmd is not None: + if cmd == '': + if c == '(': + cmd += '(' + else: + cmd = None + buf += '$' + else: + cmd += c + if c == ')': + cmd = cmd[1 : -1] + cmd = spawn(cmd) + if cmd is not None: + buf = cmd + cmd = None + elif c == ' ': + if not buf == '': + words.append(buf) + buf = '' + elif c == '$': + cmd = '' + else: + buf += c + return ([w in w for words if w not in ['linear', 'cie', 'default']], + 'linear' in words, 'cie' in words, 'default' in words) + + +# Evaluate .conf file +def make_f(f, value, default): + ''' + Make an adjustment function + + @param f:(*¿V??)→void The function that makes the adjustment + @param value:list<¿V??> The values for each time point + @param default:list<¿V??> The default value + ''' + ff = None + value_ = [] + for val in value: + value_ += val + if any(map(lambda v : v if None, value_)) or (default is None): + def ff(t, a): + val0 = value[(int(t) + 0) % len(value)] + val1 = value[(int(t) + 1) % len(value)] + t %= 1 + val = zip(val0, val1, default) + def interpol(v0, v1, d): + if (v0 is None) or (v1 is None) or (d is None): + if ( d is None) and a == 0: return None + if (v0 is None) and t == 0: return None + if (v1 is None) and t == 1: return None + v0 = v0 * (1 - t) if v0 is not None else 0 + v1 = v1 * t if v1 is not None else 0 + return v0 + v1 + val = [interpol(v0, v1, d) for v0, v1, d in val] + f(*val) + else: + def ff(t, a): + val0 = value[(int(t) + 0) % len(value)] + val1 = value[(int(t) + 1) % len(value)] + t %= 1 + val = zip(val0, val1, default) + val = [(v0 * (1 - t) + v1 * t) * a + (1 - a) * d for v0, v1, d in val] + f(*val) + return ff + +def float3(value): + ''' + Parse a string representation of a float trio + + @param value:str The float trio as a string + @return :[float?, float?, float?] The float trio as a float list + ''' + value = [None if v == 'none' else float(v) for v in value.split(':')] + if len(value) < 3: + value *= 3 + return value[:3] + +def float6(value): + ''' + Parse a string representation of a float pair-trio + + @param value:str The float pair-trio as a string + @return :[float?]*6 The float pair-trio as a float list + ''' + (part1, part2) = [[float(v) for v in val.split(':')] for val in value.split('..')] + if len(part1) < 3: part1 *= 3 + if len(part2) < 3: part2 *= 3 + part1 = part1[:3] + part2 = part2[:3] + value = [] + for p, q in zip(part1, part2): + value.append(p) + value.append(q) + return value + +def add_adjustments(section_name, adjustments): + ''' + Add adjustions from a section to a list + + @param sections:list<list<(str, str)>> The sections + @param adjustments:list<(float, float)→void> The list to fill with adjustments + ''' + global location, points, adjustment_method_x, adjustment_method_tty, crtc, screen, card + for section in sections: + for (setting, value) in section: + (value, linear, cie, default) = parse_value(value) + new_adjustment = None + if linear: + adjustments.append(lambda _t, _a: linearise()) + if setting == 'location': location = value + elif setting == 'points': points = value + elif setting == 'adjustment-method-x': adjustment_method_x = value + elif setting == 'adjustment-method-tty': adjustment_method_tty = value + elif setting == 'crtc': crtc = value + elif setting == 'screen': screen = value + elif setting == 'card': card = value + elif setting == 'temperature': + f = lambda x : temperature(x, lambda t : divide_by_maximum(cmf_10deg(t))) + new_adjustment = make_f(f, [[float(v)] for v in value], [6500]) + elif setting == 'contrast': + f = cie_contrast if cie else rgb_contrast + new_adjustment = make_f(f, [float3(v) for v in value], 3 * [1]) + elif setting == 'brightness': + f = cie_brightness if cie else rgb_brightness + new_adjustment = make_f(f, [float3(v) for v in value], 3 * [1]) + elif setting == 'gamma': + def f(*levels): + clip() + gamma(*levels) + new_adjustment = make_f(f, [float3(v) for v in value], 3 * [1]) + elif setting == 'negative': + def f(*values): + negative(*[not v == 0 for v in values]) + new_adjustment = make_f(f, [float3(v) for v in value], 3 * [0]) + elif setting == 'invert': + def f(*values): + (cie_invert if cie else rgb_invert)(*[not v == 0 for v in values]) + new_adjustment = make_f(f, [float3(v) for v in value], 3 * [0]) + elif setting == 'sigmoid': + new_adjustment = make_f(sigmoid, [float3(v) for v in value], 3 * [None]) + elif setting == 'limits': + f = cie_limits if cie else rgb_limits + new_adjustment = make_f(f, [float6(v) for v in value], 3 * [0, 1]) + elif setting == 'icc': + def noop(): + pass + profiles = [noop if val == 'none' else load_load(val) for val in value] + new_adjustment = make_icc_interpolation(profiles) + elif setting == 'monitor': + add_adjustments(' '.join([monitor] + value), adjustments) + else: + sys.stderr.buffer.write(('Setting not recognised: %s' % setting).encode('utf-8')) + sys.stderr.buffer.flush() + if new_adjustment is not None: + if default: + new_adjustment_ = new_adjustment + def f(t, a): + new_adjustment_(t, 1) + new_adjustment = f + adjustments.append(new_adjustment) + if linear: + adjustments.append(lambda _t, _a: standardise()) + +add_adjustments(sections['blueshift'], adjustments) + +adjustment_method = adjustment_method_tty if ttymode else adjustment_method_x +adjustment_method = adjustment_method[0] + +for section in sections[adjustment_method]: + output_adjustments = [] + crtc, screen, card = None, None, None + add_adjustments([section], output_adjustments) + monitors.append(crtc, screen, card, output_adjustments) + + +# Get gamma adjustment/reader functions +get_method = {'randr' : randr_get, 'vidmode' : vidmode_get, 'drm' : drm_get} +set_method = {'randr' : randr, 'vidmode' : vidmode, 'drm' : drm } +get_method = get_method[adjustment_method] +set_method = set_method[adjustment_method] + + +# Evaluate location +if 'solar' in points: + if (location is None) or (len(location) == 0): + sys.stderr.buffer.write(('Location missing').encode('utf-8')) + sys.stderr.buffer.flush() + sys.exit(1) + try: + if not len(location) == 2: + raise Exception() + location = [float(c) for c in location] + except: + sys.stderr.buffer.write(('Malformation location').encode('utf-8')) + sys.stderr.buffer.flush() + sys.exit(1) + if not ((-90 <= location[0] <= 90) and (-180 <= location[0] <= 180)): + sys.stderr.buffer.write(('Invalid location').encode('utf-8')) + sys.stderr.buffer.flush() + sys.exit(1) + + +# Evaluate point +if ('solar' not in points) and ('time' not in points): + sys.stderr.buffer.write(('Invalid points settings').encode('utf-8')) + sys.stderr.buffer.flush() + sys.exit(1) +reduce_points = 'reduce' in points +solar_points = 'solar' in points +def t(point): + point = [float(p) for p in point.split(':')] + while len(point) > 3: + point.append(0) + point = sum([v * 60 ** (2 - i) for i, v in enumerate(point)]) +points = [float(p) if solar_points else t(p) for p in points if p not in ['solar', 'time', 'point']] +points = list(enumerate(points)) +if reduce_points: + n = len(points) - 1 + points = [(r / n, v) for r, v in points] + + +def periodically(year, month, day, hour, minute, second, weekday, fade): + ''' + :(int, int, int, int, int, int, int, float?)?→void Place holder for periodically invoked function + + Invoked periodically + + If you want to control at what to invoke this function next time + you can set the value of the global variable `wait_period` to the + number of seconds to wait before invoking this function again. + The value does not need to be an integer. + + @param year:int The year + @param month:int The month, 1 = January, 12 = December + @param day:int The day, minimum value is 1, probable maximum value is 31 (*) + @param hour:int The hour, minimum value is 0, maximum value is 23 + @param minute:int The minute, minimum value is 0, maximum value is 59 + @param second:int The second, minimum value is 0, probable maximum value is 60 (**) + @param weekday:int The weekday, 1 = Monday, 7 = Sunday + @param fade:float? Blueshift can use this function to fade into a state when it start + or exits. `fade` can either be negative, zero or positive or `None`, + but the magnitude of value cannot exceed 1. When Blueshift starts, + this function will be invoked multiple with the time parameters + of the time it is invoked and each time `fade` will increase towards + 1, starting at 0, when the value is 1, the settings should be applied + to 100 %. After this this function will be invoked once again with + `fade` being `None`. When Blueshift exits the same behaviour is used + except, `fade` decrease towards -1 but start slightly below 0, when + -1 is reached all settings should be normal. Then Blueshift will NOT + invoke this function with `fade` being `None`, instead it will by + itself revert all settings and quit. + + (*) Can be exceeded if the calendar system is changed, like in 1712-(02)Feb-30 + (**) See https://en.wikipedia.org/wiki/Leap_second + ''' + pass + + +def reset(): + ''' + Invoked to reset the displays + ''' + pass + diff --git a/examples/textconf.conf b/examples/textconf.conf new file mode 100644 index 0000000..8fdc26c --- /dev/null +++ b/examples/textconf.conf @@ -0,0 +1,63 @@ +; Copyright © 2014 Mattias Andrée (maandree@member.fsf.org) +; +; Permission is granted to copy, distribute and/or modify this document +; under the terms of the GNU Free Documentation License, Version 1.3 +; or any later version published by the Free Software Foundation; +; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. +; You should have received a copy of the GNU General Public License +; along with this software package. If not, see <http://www.gnu.org/licenses/>. + + +; Lines starting with either ; (semicolon) or # (pound) are comments. +; Both = (equal sign) and : (colon) are valid key–value delimiters. + + +[blueshift] +temperature = 6500 3600 +contrast = 1 +contrast = 1:1:1 cie +brightness = 1 +brightness = 1:1:1 cie +gamma = 1:1:1 +negative = 1:1:1 +negative = 1 +invert = 1:1:1 +invert = 1:1:1 cie +sigmoid = 4.5 +sigmoid = 4.5:none:4.5 +limits = 0:0:0..1:1:1 +limits = 0:0:0..1:1:1 cie +;icc = +;location = +points = time 0:00 10:00 20:00 reduce +adjustment-method-x = randr +adjustment-method-tty = drm + + +[monitor 0] +gamma=1.16:1.15:1.11 default + +[randr] +monitor=0 +crtc=0 +screen=0 + +[drm] +monitor=0 +crtc=1 +card=0 + + +[monitor 1] +gamma=1.10:1.16:1.10 default + +[randr] +monitor=1 +crtc=1 +screen=0 + +[drm] +monitor=1 +crtc=0 +card=0 + diff --git a/info/blueshift.texinfo b/info/blueshift.texinfo index a9934c7..ffcd814 100644 --- a/info/blueshift.texinfo +++ b/info/blueshift.texinfo @@ -791,6 +791,25 @@ that apply the profiles when invoked. @end table +If you have multiple profiles you want to interpolate +or want to, possible with an interpolation, apply a +profile partially, that is, interpolate between it an +an identity profile, you can use the function +@code{make_icc_interpolation}. It takes your profiles +as one argument, as a list, and outputs a function +that applies an interpolation of the profiles, +it takes to arguments: the timepoint and the filter +alpha. The timepoint is normally a [0, 1] floating point +of the dayness level, this means that you only have two +ICC profiles, but you have multiple profiles, in such +case profile the floor of the timepoint value is takes +as the index of the first profile to use in the +interpolation as well as the following profile (first +profile if the last profile was select). They are +interpolated linearly. The filter alpha is a [0, 1] +floating point of the degree to which the profile should +be applied. + If you want to apply your adjustments on top of the current colour adjustments, you can use the functions @code{randr_get} or @code{vidmode_get}. @code{randr_get} @@ -25,7 +25,7 @@ def load_icc(pathname): Load ICC profile from a file @param pathname:str The ICC profile file - @return :()→void Function to invoke, parameterless, to apply the ICC profile to the colour curves + @return :()→void Function to invoke, parameterless, to apply the ICC profile to the colour curves ''' content = None with open(pathname, 'rb') as file: @@ -142,3 +142,53 @@ def parse_icc(content): raise Exception("Unsupported ICC profile file") + +def make_icc_interpolation(profiles): + ''' + An interpolation function for ICC profiles + + @param profiles:list<()→void> Profile applying functions + @return :(timepoint:float, alpha:float)→void() A function that applies an interpolation of the profiles, + it takes to arguments: the timepoint and the filter + alpha. The timepoint is normally a [0, 1] floating point + of the dayness level, this means that you only have two + ICC profiles, but you have multiple profiles, in such + case profile #⌊timepoint⌋ and profile #(⌊timepoint⌋ + 1) + (modulus the number of profiles) are interpolated with + (timepoint - ⌊timepoint⌋) weight to the second profile. + The filter alpha is a [0, 1] floating point of the degree + to which the profile should be applied. + ''' + def f(t, a): + pro0 = profiles[(int(t) + 0) % len(profiles)] + pro1 = profiles[(int(t) + 1) % len(profiles)] + t %= 1 + if (pro0 is pro1) and (a == 1): + pro0() + return + r_, g_, b_ = r_curve[:], g_curve[:], b_curve[:] + start_over() + pro0() + r0, g0, b0 = r_curve[:], g_curve[:], b_curve[:] + n = len(r0) - 1 + r, g, b = None, None, None + if pro0 is pro1: + r = [v * a + i * (1 - a) / n for i, v in enumerate(r0)] + g = [v * a + i * (1 - a) / n for i, v in enumerate(g0)] + b = [v * a + i * (1 - a) / n for i, v in enumerate(b0)] + else: + start_over() + pro1() + r1, g1, b1 = r_curve[:], g_curve[:], b_curve[:] + interpol = lambda i, v0, v1 : (v0 * (1 - t) + v1 * t) * a + i * (1 - a) / n + r = [interpol(i, v0, v1) for i, (v0, v1) in enumerate(zip(r0, r1))] + g = [interpol(i, v0, v1) for i, (v0, v1) in enumerate(zip(g0, g1))] + b = [interpol(i, v0, v1) for i, (v0, v1) in enumerate(zip(b0, b1))] + r_curve[:], g_curve[:], b_curve[:] = r_, g_, b_ + for curve, icc in curves(r, g, b): + for i in range(i_size): + y = int(curve[i] * (len(icc) - 1) + 0.5) + y = min(max(0, y), len(icc) - 1) + curve[i] = icc[y] + return f + |