aboutsummaryrefslogtreecommitdiffstats
path: root/src/redshift-gtk/statusicon.py
diff options
context:
space:
mode:
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()