diff options
Diffstat (limited to 'src/redshift-gtk/statusicon.py')
-rw-r--r-- | src/redshift-gtk/statusicon.py | 353 |
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() |