Make redshift-gtk read output of the redshift child process.

This allows redshift-gtk to show the current state of the redshift
process. redshift-gtk follows the enable state of redshift and toggles
the icon accordingly.

The implementation is changed to use glib to spawn the child process
instead of relying on python subprocess module. This is necessary
because of inflexibility in the python module.
This commit is contained in:
Jon Lund Steffensen
2013-12-12 00:54:40 -05:00
parent b436a6cd8e
commit 169fc05f11
+251 -104
View File
@@ -24,7 +24,8 @@ appindicator module isn't present it will fall back to a GTK status icon.
'''
import sys, os
import subprocess, signal
import signal, fcntl
import re
import gettext
from gi.repository import Gtk, GLib
@@ -36,107 +37,64 @@ except ImportError:
import defs
import utils
SUSPEND_TIMER = None
_ = gettext.gettext
def run():
# Internationalisation
gettext.bindtextdomain('redshift', defs.LOCALEDIR)
gettext.textdomain('redshift')
_ = gettext.gettext
def sigterm_handler(signal, frame):
sys.exit()
# Start redshift with arguments from the command line
args = sys.argv[1:]
args.insert(0, os.path.join(defs.BINDIR, 'redshift'))
process = subprocess.Popen(args)
try:
class RedshiftStatusIcon(object):
def __init__(self, args=[]):
# Initialize state variables
self._status = False
self._temperature = 0
self._period = 'Unknown'
self._location = (0.0, 0.0)
# Install TERM signal handler
signal.signal(signal.SIGTERM, sigterm_handler)
# Start redshift with arguments
args.insert(0, os.path.join(defs.BINDIR, 'redshift'))
if '-v' not in args:
args.append('-v')
self.start_child_process(args)
if appindicator:
# Create indicator
indicator = appindicator.Indicator.new('redshift',
'redshift-status-on',
appindicator.IndicatorCategory.APPLICATION_STATUS)
indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
self.indicator = appindicator.Indicator.new('redshift',
'redshift-status-on',
appindicator.IndicatorCategory.APPLICATION_STATUS)
self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
else:
# Create status icon
status_icon = Gtk.StatusIcon()
status_icon.set_from_icon_name('redshift-status-on')
status_icon.set_tooltip_text('Redshift')
def is_enabled():
if appindicator:
return indicator.get_icon() == 'redshift-status-on'
else:
return status_icon.get_icon_name() == 'redshift-status-on'
def remove_suspend_timer():
global SUSPEND_TIMER
if SUSPEND_TIMER is not None:
GLib.source_remove(SUSPEND_TIMER)
SUSPEND_TIMER = None
def toggle_cb(widget, data=None):
# If the user toggles redshift, we forget about the suspend timer.
# Only then widget is not None.
if widget:
remove_suspend_timer()
process.send_signal(signal.SIGUSR1)
if appindicator:
if indicator.get_icon() == 'redshift-status-on':
indicator.set_icon('redshift-status-off')
else:
indicator.set_icon('redshift-status-on')
else:
if status_icon.get_icon_name() == 'redshift-status-on':
status_icon.set_from_icon_name('redshift-status-off')
else:
status_icon.set_from_icon_name('redshift-status-on')
def enable_cb():
if is_enabled():
return
# Enable redshift
toggle_cb(None)
def suspend_cb(widget, minutes):
if is_enabled():
# Disable redshift
toggle_cb(None)
# If "suspend" is clicked while redshift is disabled, we reenable
# it after the last selected timespan is over.
remove_suspend_timer()
# If redshift was already disabled we reenable it nonetheless.
global SUSPEND_TIMER
SUSPEND_TIMER = GLib.timeout_add_seconds(minutes * 60, enable_cb)
def autostart_cb(widget, data=None):
utils.set_autostart(widget.get_active())
def destroy_cb(widget, data=None):
if not appindicator:
status_icon.set_visible(False)
Gtk.main_quit()
return False
self.status_icon = Gtk.StatusIcon()
self.status_icon.set_from_icon_name('redshift-status-on')
self.status_icon.set_tooltip_text('Redshift')
# Create popup menu
status_menu = Gtk.Menu()
self.status_menu = Gtk.Menu()
# Add toggle action
toggle_item = Gtk.MenuItem(_('Toggle'))
toggle_item.connect('activate', toggle_cb)
status_menu.append(toggle_item)
toggle_item.connect('activate', self.toggle_cb)
self.status_menu.append(toggle_item)
# Add suspend menu
suspend_menu_item = Gtk.MenuItem(_('Suspend for'))
suspend_menu = Gtk.Menu()
for minutes, label in [(30, _('30 minutes')), (60, _('1 hour')),
for minutes, label in [(30, _('30 minutes')),
(60, _('1 hour')),
(120, _('2 hours'))]:
suspend_item = Gtk.MenuItem(label)
suspend_item.connect('activate', suspend_cb, minutes)
suspend_item.connect('activate', self.suspend_cb, minutes)
suspend_menu.append(suspend_item)
suspend_menu_item.set_submenu(suspend_menu)
status_menu.append(suspend_menu_item)
self.status_menu.append(suspend_menu_item)
# Add autostart option
autostart_item = Gtk.CheckMenuItem(_('Autostart'))
try:
autostart_item.set_active(utils.get_autostart())
@@ -144,45 +102,234 @@ def run():
print strerror
autostart_item.set_property('sensitive', False)
else:
autostart_item.connect('activate', autostart_cb)
autostart_item.connect('toggled', self.autostart_cb)
finally:
status_menu.append(autostart_item)
self.status_menu.append(autostart_item)
# Add info action
info_item = Gtk.MenuItem(_('Info'))
info_item.connect('activate', self.show_info_cb)
self.status_menu.append(info_item)
# Add quit action
quit_item = Gtk.ImageMenuItem(_('Quit'))
quit_item.connect('activate', destroy_cb)
status_menu.append(quit_item)
quit_item.connect('activate', self.destroy_cb)
self.status_menu.append(quit_item)
# Create info dialog
self.info_dialog = Gtk.Dialog(_('Info'),
None, 0,
(_('Close'), Gtk.ResponseType.CLOSE))
self.info_dialog.set_resizable(False)
self.info_dialog.set_property('border-width', 6)
self.status_label = Gtk.Label()
self.status_label.set_alignment(0.0, 0.5)
self.status_label.set_padding(6, 6);
self.info_dialog.get_content_area().pack_start(self.status_label, True, True, 0)
self.status_label.show()
self.location_label = Gtk.Label()
self.location_label.set_alignment(0.0, 0.5)
self.location_label.set_padding(6, 6);
self.info_dialog.get_content_area().pack_start(self.location_label, True, True, 0)
self.location_label.show()
self.temperature_label = Gtk.Label()
self.temperature_label.set_alignment(0.0, 0.5)
self.temperature_label.set_padding(6, 6);
self.info_dialog.get_content_area().pack_start(self.temperature_label, True, True, 0)
self.temperature_label.show()
self.period_label = Gtk.Label()
self.period_label.set_alignment(0.0, 0.5)
self.period_label.set_padding(6, 6);
self.info_dialog.get_content_area().pack_start(self.period_label, True, True, 0)
self.period_label.show()
self.info_dialog.connect('response', self.response_info_cb)
if appindicator:
status_menu.show_all()
self.status_menu.show_all()
# Set the menu
indicator.set_menu(status_menu)
self.indicator.set_menu(self.status_menu)
else:
def popup_menu_cb(widget, button, time, data=None):
status_menu.show_all()
status_menu.popup(None, None,
Gtk.StatusIcon.position_menu,
status_icon, button, time)
# Connect signals for status icon and show
status_icon.connect('activate', toggle_cb)
status_icon.connect('popup-menu', popup_menu_cb)
status_icon.set_visible(True)
self.status_icon.connect('activate', self.toggle_cb)
self.status_icon.connect('popup-menu', self.popup_menu_cb)
self.status_icon.set_visible(True)
def child_cb(pid, cond, data=None):
sys.exit(-1)
# Initialize suspend timer
self.suspend_timer = None
# Handle child input
class InputBuffer(object):
lines = []
buf = ''
self.input_buffer = InputBuffer()
self.error_buffer = InputBuffer()
# Set non blocking
fcntl.fcntl(self.process[2], fcntl.F_SETFL,
fcntl.fcntl(self.process[2], fcntl.F_GETFL) | os.O_NONBLOCK)
# Add watch on child process
GLib.child_watch_add(process.pid, child_cb)
GLib.child_watch_add(self.process[0], self.child_cb)
GLib.io_add_watch(self.process[2], GLib.IO_IN,
self.child_data_cb, (True, self.input_buffer))
GLib.io_add_watch(self.process[3], GLib.IO_IN,
self.child_data_cb, (False, self.error_buffer))
def start_child_process(self, args):
# Start child process with C locale so we can parse the output
env = os.environ.copy()
env['LANG'] = env['LANGUAGE'] = env['LC_ALL'] = env['LC_MESSAGES'] = 'C'
self.process = GLib.spawn_async(args, envp=['{}={}'.format(k,v) for k, v in env.iteritems()],
flags=GLib.SPAWN_DO_NOT_REAP_CHILD,
standard_output=True, standard_error=True)
def remove_suspend_timer(self):
if self.suspend_timer is not None:
GLib.source_remove(self.suspend_timer)
self.suspend_timer = None
def suspend_cb(self, item, minutes):
if self.is_enabled():
self.child_toggle_status()
# If "suspend" is clicked while redshift is disabled, we reenable
# it after the last selected timespan is over.
self.remove_suspend_timer()
# If redshift was already disabled we reenable it nonetheless.
self.suspend_timer = GLib.timeout_add_seconds(minutes * 60, self.reenable_cb)
def reenable_cb(self):
if not self.is_enabled():
self.child_toggle_status()
def popup_menu_cb(self, widget, button, time, data=None):
self.status_menu.show_all()
self.status_menu.popup(None, None, Gtk.StatusIcon.position_menu,
self.status_icon, button, time)
def toggle_cb(self, widget, data=None):
self.remove_suspend_timer()
self.child_toggle_status()
# Info dialog callbacks
def show_info_cb(self, widget, data=None):
self.info_dialog.show()
def response_info_cb(self, widget, data=None):
self.info_dialog.hide()
def update_status_icon(self):
# Update status icon
if appindicator:
if self.is_enabled():
self.indicator.set_icon('redshift-status-on')
else:
self.indicator.set_icon('redshift-status-off')
else:
if self.is_enabled():
self.status_icon.set_from_icon_name('redshift-status-on')
else:
self.status_icon.set_from_icon_name('redshift-status-off')
# Status update functions. Called when child process indicates a
# change in state.
def change_status(self, status):
self._status = status
# TODO make toggle_item a checkbox and follow the state like the icon.
self.update_status_icon()
self.status_label.set_markup('<b>{}:</b> {}'.format(_('Status'),
_('Enabled') if status else _('Disabled')))
def change_temperature(self, temperature):
self._temperature = temperature
self.temperature_label.set_markup('<b>{}:</b> {}K'.format(_('Color temperature'), temperature))
def change_period(self, period):
self._period = period
self.period_label.set_markup('<b>{}:</b> {}'.format(_('Period'), period))
def change_location(self, location):
self._location = location
self.location_label.set_markup('<b>{}:</b> {}, {}'.format(_('Location'), *location))
def is_enabled(self):
return self._status
def autostart_cb(self, widget, data=None):
utils.set_autostart(widget.get_active())
def destroy_cb(self, widget, data=None):
if not appindicator:
self.status_icon.set_visible(False)
Gtk.main_quit()
return False
def child_toggle_status(self):
os.kill(self.process[0], signal.SIGUSR1)
def child_cb(self, pid, cond, data=None):
sys.exit(-1)
def child_key_change_cb(self, key, value):
if key == 'Status':
self.change_status(value != 'Disabled')
elif key == 'Color temperature':
self.change_temperature(int(value[:-1], 10))
elif key == 'Period':
self.change_period(value)
elif key == 'Location':
self.change_location(tuple(float(x) for x in value.split(', ')))
def child_stdout_line_cb(self, line):
if line:
m = re.match(r'([\w ]+): (.+)', line)
if m:
key = m.group(1)
value = m.group(2)
self.child_key_change_cb(key, value)
def child_data_cb(self, f, cond, data):
stdout, ib = data
ib.buf += os.read(f, 256)
# Split input at line break
sep = True
while sep != '':
first, sep, last = ib.buf.partition('\n')
ib.buf = last
ib.lines.append(first)
if stdout:
self.child_stdout_line_cb(first)
return True
def termwait(self):
os.kill(self.process[0], signal.SIGINT)
os.waitpid(self.process[0], 0)
def run():
# Internationalisation
gettext.bindtextdomain('redshift', defs.LOCALEDIR)
gettext.textdomain('redshift')
# Create status icon
s = RedshiftStatusIcon(sys.argv[1:])
try:
# Run main loop
Gtk.main()
except KeyboardInterrupt:
# Ignore user interruption
pass
finally:
# Always terminate redshift
process.terminate()
process.wait()
s.termwait()