aboutsummaryrefslogtreecommitdiffstats
path: root/src/redshift-gtk/statusicon.py
diff options
context:
space:
mode:
authorJon Lund Steffensen <jonlst@gmail.com>2013-03-02 12:39:24 +0100
committerJon Lund Steffensen <jonlst@gmail.com>2013-12-12 00:54:40 -0500
commit169fc05f11987461a03c08a2aef8637f87b2c848 (patch)
treeb6be5fa73b8eb4e5c875384d3d1b411644c13efd /src/redshift-gtk/statusicon.py
parentredshift-gtk: Use new GIR Gtk/Glib/AppIndicator bindings (diff)
downloadredshift-ng-169fc05f11987461a03c08a2aef8637f87b2c848.tar.gz
redshift-ng-169fc05f11987461a03c08a2aef8637f87b2c848.tar.bz2
redshift-ng-169fc05f11987461a03c08a2aef8637f87b2c848.tar.xz
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.
Diffstat (limited to 'src/redshift-gtk/statusicon.py')
-rw-r--r--src/redshift-gtk/statusicon.py353
1 files changed, 250 insertions, 103 deletions
diff --git a/src/redshift-gtk/statusicon.py b/src/redshift-gtk/statusicon.py
index 1804582..15a822b 100644
--- a/src/redshift-gtk/statusicon.py
+++ b/src/redshift-gtk/statusicon.py
@@ -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
+_ = gettext.gettext
-SUSPEND_TIMER = None
+def sigterm_handler(signal, frame):
+ sys.exit()
-def run():
- # Internationalisation
- gettext.bindtextdomain('redshift', defs.LOCALEDIR)
- gettext.textdomain('redshift')
- _ = gettext.gettext
- # 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)
+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)
- try:
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)
+
+ # Initialize suspend timer
+ self.suspend_timer = None
- def child_cb(pid, cond, data=None):
- sys.exit(-1)
+ # 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()