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.py434
1 files changed, 150 insertions, 284 deletions
diff --git a/src/redshift-gtk/statusicon.py b/src/redshift-gtk/statusicon.py
index b766175..b4adfb0 100644
--- a/src/redshift-gtk/statusicon.py
+++ b/src/redshift-gtk/statusicon.py
@@ -14,243 +14,61 @@
# You should have received a copy of the GNU General Public License
# along with Redshift. If not, see <http://www.gnu.org/licenses/>.
-# Copyright (c) 2013-2014 Jon Lund Steffensen <jonlst@gmail.com>
+# Copyright (c) 2013-2017 Jon Lund Steffensen <jonlst@gmail.com>
-'''GUI status icon for Redshift.
+"""GUI status icon for Redshift.
The run method will try to start an appindicator for Redshift. If the
appindicator module isn't present it will fall back to a GTK status icon.
-'''
+"""
-import sys, os
-import fcntl
+import sys
import signal
-import re
import gettext
-from gi.repository import Gtk, GLib, GObject
+import gi
+gi.require_version('Gtk', '3.0')
+
+from gi.repository import Gtk, GLib
try:
+ gi.require_version('AppIndicator3', '0.1')
from gi.repository import AppIndicator3 as appindicator
-except ImportError:
+except (ImportError, ValueError):
appindicator = None
+from .controller import RedshiftController
from . import defs
from . import utils
_ = gettext.gettext
-class RedshiftController(GObject.GObject):
- '''A GObject wrapper around the child process'''
-
- __gsignals__ = {
- 'inhibit-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)),
- 'temperature-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
- 'period-changed': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
- 'location-changed': (GObject.SIGNAL_RUN_FIRST, None, (float, float)),
- 'error-occured': (GObject.SIGNAL_RUN_FIRST, None, (str,))
- }
-
- 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.'''
-
- GObject.GObject.__init__(self)
-
- # Initialize state variables
- self._inhibited = False
- self._temperature = 0
- self._period = 'Unknown'
- self._location = (0.0, 0.0)
-
- # Start redshift with arguments
- args.insert(0, os.path.join(defs.BINDIR, 'redshift'))
- if '-v' not in args:
- args.insert(1, '-v')
-
- # 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.items()],
- flags=GLib.SPAWN_DO_NOT_REAP_CHILD,
- standard_output=True, standard_error=True)
-
- # Wrap remaining contructor in try..except to avoid that the child
- # process is not closed properly.
- try:
- # Handle child input
- # The buffer is encapsulated in a class so we
- # can pass an instance to the child callback.
- class InputBuffer(object):
- buf = ''
-
- self._input_buffer = InputBuffer()
- self._error_buffer = InputBuffer()
- self._errors = ''
-
- # 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(GLib.PRIORITY_DEFAULT, self._process[0], self._child_cb)
- GLib.io_add_watch(self._process[2], GLib.PRIORITY_DEFAULT, GLib.IO_IN,
- self._child_data_cb, (True, self._input_buffer))
- GLib.io_add_watch(self._process[3], GLib.PRIORITY_DEFAULT, GLib.IO_IN,
- self._child_data_cb, (False, self._error_buffer))
-
- # Signal handler to relay USR1 signal to redshift process
- def relay_signal_handler(signal):
- os.kill(self._process[0], signal)
- return True
-
- GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGUSR1,
- relay_signal_handler, signal.SIGUSR1)
- except:
- self.termwait()
- raise
-
- @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:
- self._child_toggle_inhibit()
-
- def _child_toggle_inhibit(self):
- '''Sends a request to the child process to toggle state'''
- os.kill(self._process[0], signal.SIGUSR1)
-
- def _child_cb(self, pid, status, data=None):
- '''Called when the child process exists'''
-
- # Empty stdout and stderr
- for f in (self._process[2], self._process[3]):
- while True:
- buf = os.read(f, 256).decode('utf-8')
- if buf == '':
- break
- if f == self._process[3]: # stderr
- self._errors += buf
-
- # Check exit status of child
- report_errors = False
- try:
- GLib.spawn_check_exit_status(status)
- Gtk.main_quit()
- except GLib.GError:
- report_errors = True
-
- if report_errors:
- self.emit('error-occured', self._errors)
-
- def _child_key_change_cb(self, key, value):
- '''Called when the child process reports a change of internal state'''
-
- 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
- self.emit('inhibit-changed', new_inhibited)
- elif key == 'Color temperature':
- new_temperature = int(value.rstrip('K'), 10)
- if new_temperature != self._temperature:
- self._temperature = new_temperature
- self.emit('temperature-changed', new_temperature)
- elif key == 'Period':
- new_period = value
- if new_period != self._period:
- self._period = new_period
- self.emit('period-changed', 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
- self.emit('location-changed', *new_location)
-
- def _child_stdout_line_cb(self, line):
- '''Called when the child process outputs a line to stdout'''
- 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):
- '''Called when the child process has new data on stdout/stderr'''
-
- stdout, ib = data
- ib.buf += os.read(f, 256).decode('utf-8')
-
- # Split input at line break
- while True:
- first, sep, last = ib.buf.partition('\n')
- if sep == '':
- break
- ib.buf = last
- if stdout:
- self._child_stdout_line_cb(first)
- else:
- self._errors += first + '\n'
-
- return True
-
- def termwait(self):
- '''Send SIGINT and wait for the child process to quit'''
- try:
- os.kill(self._process[0], signal.SIGINT)
- os.waitpid(self._process[0], 0)
- except ProcessLookupError:
- # Process has apparently already disappeared
- pass
-
-
class RedshiftStatusIcon(object):
- '''The status icon tracking the RedshiftController'''
+ """The status icon tracking the RedshiftController."""
def __init__(self, controller):
- '''Creates a new instance of the status icon'''
+ """Creates a new instance of the status icon."""
self._controller = controller
+ self.icon_theme = Gtk.IconTheme.get_default()
+ icon_name = 'redshift-status-on-symbolic'
+ if not self.icon_theme.has_icon(icon_name):
+ icon_name = 'redshift-status-on'
+
if appindicator:
# Create indicator
- self.indicator = appindicator.Indicator.new('redshift',
- 'redshift-status-on',
- appindicator.IndicatorCategory.APPLICATION_STATUS)
+ self.indicator = appindicator.Indicator.new(
+ 'redshift',
+ icon_name,
+ appindicator.IndicatorCategory.APPLICATION_STATUS)
self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
else:
# Create status icon
self.status_icon = Gtk.StatusIcon()
- self.status_icon.set_from_icon_name('redshift-status-on')
+ self.status_icon.set_from_icon_name(icon_name)
self.status_icon.set_tooltip_text('Redshift')
# Create popup menu
@@ -266,7 +84,9 @@ class RedshiftStatusIcon(object):
suspend_menu = Gtk.Menu()
for minutes, label in [(30, _('30 minutes')),
(60, _('1 hour')),
- (120, _('2 hours'))]:
+ (120, _('2 hours')),
+ (240, _('4 hours')),
+ (480, _('8 hours'))]:
suspend_item = Gtk.MenuItem.new_with_label(label)
suspend_item.connect('activate', self.suspend_cb, minutes)
suspend_menu.append(suspend_item)
@@ -274,16 +94,17 @@ class RedshiftStatusIcon(object):
self.status_menu.append(suspend_menu_item)
# Add autostart option
- autostart_item = Gtk.CheckMenuItem.new_with_label(_('Autostart'))
- try:
- autostart_item.set_active(utils.get_autostart())
- except IOError as strerror:
- print(strerror)
- autostart_item.set_property('sensitive', False)
- else:
- autostart_item.connect('toggled', self.autostart_cb)
- finally:
- self.status_menu.append(autostart_item)
+ if utils.supports_autostart():
+ autostart_item = Gtk.CheckMenuItem.new_with_label(_('Autostart'))
+ try:
+ autostart_item.set_active(utils.get_autostart())
+ except IOError as strerror:
+ print(strerror)
+ autostart_item.set_property('sensitive', False)
+ else:
+ autostart_item.connect('toggled', self.autostart_cb)
+ finally:
+ self.status_menu.append(autostart_item)
# Add info action
info_item = Gtk.MenuItem.new_with_label(_('Info'))
@@ -296,44 +117,53 @@ class RedshiftStatusIcon(object):
self.status_menu.append(quit_item)
# Create info dialog
- self.info_dialog = Gtk.Dialog()
- self.info_dialog.set_title(_('Info'))
- self.info_dialog.add_button(_('Close'), Gtk.ButtonsType.CLOSE)
+ self.info_dialog = Gtk.Window(title=_('Info'))
self.info_dialog.set_resizable(False)
self.info_dialog.set_property('border-width', 6)
+ self.info_dialog.connect('delete-event', self.close_info_dialog_cb)
+
+ content_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
+ self.info_dialog.add(content_area)
+ content_area.show()
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)
+ 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)
+ 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)
+ 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)
+ content_area.pack_start(self.period_label, True, True, 0)
self.period_label.show()
- self.info_dialog.connect('response', self.response_info_cb)
+ self.close_button = Gtk.Button(label=_('Close'))
+ content_area.pack_start(self.close_button, True, True, 0)
+ self.close_button.connect('clicked', self.close_info_dialog_cb)
+ self.close_button.show()
# Setup signals to property changes
self._controller.connect('inhibit-changed', self.inhibit_change_cb)
self._controller.connect('period-changed', self.period_change_cb)
- self._controller.connect('temperature-changed', self.temperature_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)
+ self._controller.connect('stopped', self.controller_stopped_cb)
+ self.icon_theme.connect('changed', self.on_icon_theme_changed_cb)
# Set info box text
self.change_inhibited(self._controller.inhibited)
@@ -356,18 +186,18 @@ class RedshiftStatusIcon(object):
self.suspend_timer = None
def remove_suspend_timer(self):
- '''Disable any previously set suspend timer'''
+ """Disable any previously set suspend timer."""
if self.suspend_timer is not None:
GLib.source_remove(self.suspend_timer)
self.suspend_timer = None
def suspend_cb(self, item, minutes):
- '''Callback that handles activation of a suspend timer
-
- The minutes parameter is the number of minutes to suspend. Even if redshift
- is not disabled when called, it will still set a suspend timer and
- reactive redshift when the timer is up.'''
+ """Callback that handles activation of a suspend timer.
+ The minutes parameter is the number of minutes to suspend. Even if
+ redshift is not disabled when called, it will still set a suspend timer
+ and reactive redshift when the timer is up.
+ """
# Inhibit
self._controller.set_inhibit(True)
@@ -376,29 +206,30 @@ class RedshiftStatusIcon(object):
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)
+ self.suspend_timer = GLib.timeout_add_seconds(
+ minutes * 60, self.reenable_cb)
def reenable_cb(self):
- '''Callback to reenable redshift when a suspend timer expires'''
+ """Callback to reenable redshift when a suspend timer expires."""
self._controller.set_inhibit(False)
def popup_menu_cb(self, widget, button, time, data=None):
- '''Callback when the popup menu on the status icon has to open'''
+ """Callback when the popup menu on the status icon has to open."""
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):
- '''Callback when a request to toggle redshift was made'''
+ """Callback when a request to toggle redshift was made."""
self.remove_suspend_timer()
self._controller.set_inhibit(not self._controller.inhibited)
def toggle_item_cb(self, widget, data=None):
- '''Callback then a request to toggle redshift was made from a toggle item
+ """Callback when a request to toggle redshift was made.
This ensures that the state of redshift is synchronised with
- the toggle state of the widget (e.g. Gtk.CheckMenuItem).'''
-
+ the toggle state of the widget (e.g. Gtk.CheckMenuItem).
+ """
active = not self._controller.inhibited
if active != widget.get_active():
self.remove_suspend_timer()
@@ -406,115 +237,150 @@ class RedshiftStatusIcon(object):
# Info dialog callbacks
def show_info_cb(self, widget, data=None):
- '''Callback when the info dialog should be presented'''
+ """Callback when the info dialog should be presented."""
self.info_dialog.show()
def response_info_cb(self, widget, data=None):
- '''Callback when a button in the info dialog was activated'''
+ """Callback when a button in the info dialog was activated."""
self.info_dialog.hide()
+ def close_info_dialog_cb(self, widget, data=None):
+ """Callback when the info dialog is closed."""
+ self.info_dialog.hide()
+ return True
+
+ def on_icon_theme_changed_cb(self, theme):
+ self.update_status_icon()
+
def update_status_icon(self):
- '''Update the status icon according to the internally recorded state
+ """Update the status icon according to the internally recorded state.
This should be called whenever the internally recorded state
- might have changed.'''
+ might have changed.
+ """
+ if self._controller.inhibited:
+ icon_name = 'redshift-status-off-symbolic'
+ else:
+ icon_name = 'redshift-status-on-symbolic'
+
+ if not self.icon_theme.has_icon(icon_name):
+ icon_name = icon_name.replace('-symbolic', '')
- # Update status icon
if appindicator:
- if not self._controller.inhibited:
- self.indicator.set_icon('redshift-status-on')
- else:
- self.indicator.set_icon('redshift-status-off')
+ self.indicator.set_icon(icon_name)
else:
- if not self._controller.inhibited:
- self.status_icon.set_from_icon_name('redshift-status-on')
- else:
- self.status_icon.set_from_icon_name('redshift-status-off')
+ self.status_icon.set_from_icon_name(icon_name)
# State update functions
def inhibit_change_cb(self, controller, inhibit):
- '''Callback when controller changes inhibition status'''
+ """Callback when controller changes inhibition status."""
self.change_inhibited(inhibit)
def period_change_cb(self, controller, period):
- '''Callback when controller changes period'''
+ """Callback when controller changes period."""
self.change_period(period)
def temperature_change_cb(self, controller, temperature):
- '''Callback when controller changes temperature'''
+ """Callback when controller changes temperature."""
self.change_temperature(temperature)
def location_change_cb(self, controller, lat, lon):
- '''Callback when controlled changes location'''
+ """Callback when controlled changes location."""
self.change_location((lat, lon))
def error_occured_cb(self, controller, error):
- '''Callback when an error occurs in the controller'''
- error_dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
- Gtk.ButtonsType.CLOSE, '')
- error_dialog.set_markup('<b>Failed to run Redshift</b>\n<i>' + error + '</i>')
+ """Callback when an error occurs in the controller."""
+ error_dialog = Gtk.MessageDialog(
+ None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
+ Gtk.ButtonsType.CLOSE, '')
+ error_dialog.set_markup(
+ '<b>Failed to run Redshift</b>\n<i>' + error + '</i>')
error_dialog.run()
# Quit when the model dialog is closed
sys.exit(-1)
+ def controller_stopped_cb(self, controller):
+ """Callback when controlled is stopped successfully."""
+ Gtk.main_quit()
+
# Update interface
def change_inhibited(self, inhibited):
- '''Change interface to new inhibition status'''
+ """Change interface to new inhibition status."""
self.update_status_icon()
self.toggle_item.set_active(not inhibited)
- self.status_label.set_markup(_('<b>Status:</b> {}').format(_('Disabled') if inhibited else _('Enabled')))
+ self.status_label.set_markup(
+ _('<b>Status:</b> {}').format(
+ _('Disabled') if inhibited else _('Enabled')))
def change_temperature(self, temperature):
- '''Change interface to new temperature'''
- self.temperature_label.set_markup('<b>{}:</b> {}K'.format(_('Color temperature'), temperature))
+ """Change interface to new temperature."""
+ self.temperature_label.set_markup(
+ '<b>{}:</b> {}K'.format(_('Color temperature'), temperature))
+ self.update_tooltip_text()
def change_period(self, period):
- '''Change interface to new period'''
- self.period_label.set_markup('<b>{}:</b> {}'.format(_('Period'), period))
+ """Change interface to new period."""
+ self.period_label.set_markup(
+ '<b>{}:</b> {}'.format(_('Period'), period))
+ self.update_tooltip_text()
def change_location(self, location):
- '''Change interface to new location'''
- self.location_label.set_markup('<b>{}:</b> {}, {}'.format(_('Location'), *location))
+ """Change interface to new location."""
+ self.location_label.set_markup(
+ '<b>{}:</b> {}, {}'.format(_('Location'), *location))
+ def update_tooltip_text(self):
+ """Update text of tooltip status icon."""
+ if not appindicator:
+ self.status_icon.set_tooltip_text('{}: {}K, {}: {}'.format(
+ _('Color temperature'), self._controller.temperature,
+ _('Period'), self._controller.period))
def autostart_cb(self, widget, data=None):
- '''Callback when a request to toggle autostart is made'''
+ """Callback when a request to toggle autostart is made."""
utils.set_autostart(widget.get_active())
def destroy_cb(self, widget, data=None):
- '''Callback when a request to quit the application is made'''
+ """Callback when a request to quit the application is made."""
if not appindicator:
self.status_icon.set_visible(False)
- Gtk.main_quit()
+ self.info_dialog.destroy()
+ self._controller.terminate_child()
return False
-def sigterm_handler(data=None):
- sys.exit(0)
-
-
def run():
utils.setproctitle('redshift-gtk')
- # Install TERM signal handler
- GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGTERM,
- sigterm_handler, None)
- GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT,
- sigterm_handler, None)
-
# Internationalisation
gettext.bindtextdomain('redshift', defs.LOCALEDIR)
gettext.textdomain('redshift')
+ for help_arg in ('-h', '--help'):
+ if help_arg in sys.argv:
+ print(_('Please run `redshift -h` for help output.'))
+ sys.exit(-1)
+
# Create redshift child process controller
c = RedshiftController(sys.argv[1:])
+
+ def terminate_child(data=None):
+ c.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:
# Create status icon
- s = RedshiftStatusIcon(c)
+ RedshiftStatusIcon(c)
# Run main loop
Gtk.main()
- finally:
- # Always make sure that the child process is closed
- c.termwait()
+ except:
+ c.kill_child()
+ raise