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.
This commit is contained in:
Thiago Kenji Okada 2016-10-25 23:09:11 -02:00
parent de1fe9b72c
commit 9a8c1011a8
2 changed files with 111 additions and 56 deletions

View File

@ -47,7 +47,6 @@ MOCK_MODULES = [
"i3ipc",
"dateutil.parser",
"dateutil.relativedelta",
"redshift_gtk.statusicon",
"xkbgroup",
]

View File

@ -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),