From 9a8c1011a8749276f769fe0ff627a576acec6bfa Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Tue, 25 Oct 2016 23:09:11 -0200 Subject: [PATCH] Refactor redshift module Now using a internal RedshiftController, based on the original statusbar.py, however written in pure Python instead of depending in GLib. Since it does not steal SIGUSR1 handler, it should fix issue #482. --- docs/conf.py | 1 - i3pystatus/redshift.py | 166 +++++++++++++++++++++++++++-------------- 2 files changed, 111 insertions(+), 56 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 87c5fc9..a3872ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,6 @@ MOCK_MODULES = [ "i3ipc", "dateutil.parser", "dateutil.relativedelta", - "redshift_gtk.statusicon", "xkbgroup", ] diff --git a/i3pystatus/redshift.py b/i3pystatus/redshift.py index 545781e..b784afc 100644 --- a/i3pystatus/redshift.py +++ b/i3pystatus/redshift.py @@ -1,14 +1,110 @@ +import os +import re import signal import threading - -import gi -gi.require_version('Gtk', '3.0') # nopep8 -from gi.repository import Gtk, GLib -from redshift_gtk.statusicon import RedshiftController +from subprocess import Popen, PIPE, CalledProcessError from i3pystatus import IntervalModule, formatp +class RedshiftController(threading.Thread): + + def __init__(self, args=""): + """Initialize controller and start child process + + The parameter args is a list of command line arguments to pass on to + the child process. The "-v" argument is automatically added.""" + + threading.Thread.__init__(self) + + # Regex to parse output + self._regex = re.compile(r'([\w ]+): (.+)') + + # Initialize state variables + self._inhibited = False + self._temperature = 0 + self._period = 'Unknown' + self._location = (0.0, 0.0) + + cmd = ["redshift"] + if "-v" not in cmd: + cmd += ["-v"] + + env = os.environ.copy() + env['LANG'] = env['LANGUAGE'] = env['LC_ALL'] = env['LC_MESSAGES'] = 'C' + self._p = Popen(cmd, env=env, stdout=PIPE, bufsize=1, universal_newlines=True) + + def parse_output(self, line): + """Convert output to key value pairs""" + + if line: + m = self._regex.match(line) + if m: + key = m.group(1) + value = m.group(2) + self.update_value(key, value) + + def update_value(self, key, value): + """Parse key value pairs to update their values""" + + def parse_coord(s): + """Parse coordinate like `42.0 N` or `91.5 W`""" + v, d = s.split(' ') + return float(v) * (1 if d in 'NE' else -1) + + if key == 'Status': + new_inhibited = value != 'Enabled' + if new_inhibited != self._inhibited: + self._inhibited = new_inhibited + elif key == 'Color temperature': + new_temperature = int(value.rstrip('K'), 10) + if new_temperature != self._temperature: + self._temperature = new_temperature + elif key == 'Period': + new_period = value + if new_period != self._period: + self._period = new_period + elif key == 'Location': + new_location = tuple(parse_coord(x) for x in value.split(', ')) + if new_location != self._location: + self._location = new_location + + @property + def inhibited(self): + """Current inhibition state""" + return self._inhibited + + @property + def temperature(self): + """Current screen temperature""" + return self._temperature + + @property + def period(self): + """Current period of day""" + return self._period + + @property + def location(self): + """Current location""" + return self._location + + def set_inhibit(self, inhibit): + """Set inhibition state""" + if inhibit != self._inhibited: + os.kill(self._p.pid, signal.SIGUSR1) + self._inhibited = inhibit + + def run(self): + for line in self._p.stdout: + self.parse_output(line) + + self._p.stdout.close() + return_code = self._p.wait() + if return_code != 0: + raise CalledProcessError("redshift exited with {} return code".format(return_code)) + + class Redshift(IntervalModule): """ Show status and control redshift - http://jonls.dk/redshift/. @@ -17,7 +113,7 @@ class Redshift(IntervalModule): its output, so you should remove redshift/redshift-gtk from your i3 config before using this module. - Requires `redshift` and `redshift-gtk`. + Requires `redshift` installed. .. rubric:: Available formatters @@ -46,56 +142,15 @@ class Redshift(IntervalModule): def init(self): self._controller = RedshiftController(self.redshift_parameters) + self._controller.daemon = True + self._controller.start() + self.update_values() + def update_values(self): self.inhibit = self._controller.inhibited self.period = self._controller.period self.temperature = self._controller.temperature self.latitude, self.longitude = self._controller.location - self.error = "" - - # Setup signals to property changes - self._controller.connect('period-changed', self.period_change_cb) - self._controller.connect('temperature-changed', self.temperature_change_cb) - self._controller.connect('location-changed', self.location_change_cb) - self._controller.connect('error-occured', self.error_occured_cb) - - def terminate_child(data=None): - self._controller.terminate_child() - return False - - # Install signal handlers - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGTERM, - terminate_child, None) - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, - terminate_child, None) - - try: - t = threading.Thread(target=Gtk.main) - t.daemon = True - t.start() - except Exception as e: - self._controller.kill_child() - self.output = { - "full_text": "Error creating new thread!", - "color": self.error_color - } - - def period_change_cb(self, controller, period): - """Callback when controller changes period""" - self.period = period - - def temperature_change_cb(self, controller, temperature): - """Callback when controller changes temperature""" - self.temperature = temperature - - def location_change_cb(self, controller, latitude, longitude): - """Callback when controlled changes location""" - self.latitude = latitude - self.longitude = longitude - - def error_occured_cb(self, controller, error): - """Callback when an error occurs in the controller""" - self.error = error def toggle_inhibit(self): """Enable/disable redshift""" @@ -107,10 +162,8 @@ class Redshift(IntervalModule): self.inhibit = True def run(self): - if self.error: - fdict = {"error": self.error} - color = self.error_color - else: + self.update_values() + try: fdict = { "inhibit": self.format_inhibit[int(self.inhibit)], "period": self.period, @@ -119,6 +172,9 @@ class Redshift(IntervalModule): "longitude": self.longitude, } color = self.color + except CalledProcessError as err: + fdict = {"error": err} + color = self.error_color self.output = { "full_text": formatp(self.format, **fdict),