diff --git a/Makefile.am b/Makefile.am index 675fda82..8c1e19a6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -33,6 +33,9 @@ SYSTEMD_USER_UNIT_IN_FILES = \ data/systemd/redshift.service.in \ data/systemd/redshift-gtk.service.in +DBUS_SERVICE_IN_FILES = \ + data/dbus-1/services/dk.jonls.redshift.Redshift.service.in + # Icons if ENABLE_GUI @@ -78,6 +81,16 @@ $(systemduserunit_DATA): $(SYSTEMD_USER_UNIT_IN_FILES) Makefile sed -e "s|\@bindir\@|$(bindir)|g" $< > $@ +# D-Bus service file +if ENABLE_DBUS +dbus_servicedir = @datadir@/dbus-1/services +dbus_service_DATA = $(DBUS_SERVICE_IN_FILES:.service.in=.service) +endif + +$(dbus_service_DATA): $(DBUS_SERVICE_IN_FILES) Makefile + $(AM_V_GEN)$(MKDIR_P) $(@D) && \ + sed -e "s|\@libexecdir\@|$(libexecdir)|" $< > $@ + EXTRA_DIST = \ $(EXTRA_ROOTDOC_FILES) \ @@ -85,9 +98,11 @@ EXTRA_DIST = \ $(_UBUNTU_MONO_DARK_FILES) \ $(_UBUNTU_MONO_LIGHT_FILES) \ $(_DESKTOP_FILES) \ - $(SYSTEMD_USER_UNIT_IN_FILES) + $(SYSTEMD_USER_UNIT_IN_FILES) \ + $(DBUS_SERVICE_IN_FILES) -CLEANFILES = $(systemduserunit_DATA) +CLEANFILES = $(systemduserunit_DATA) \ + $(dbus_service_DATA) # Update PO translations diff --git a/configure.ac b/configure.ac index deae0cfc..d50933f3 100644 --- a/configure.ac +++ b/configure.ac @@ -25,6 +25,7 @@ PKG_CHECK_MODULES([XCB_RANDR], [xcb-randr], [have_xcb_randr=yes], [have_xcb_randr=no]) PKG_CHECK_MODULES([GLIB], [glib-2.0 gobject-2.0], [have_glib=yes], [have_glib=no]) +PKG_CHECK_MODULES([GDBUS], [gio-2.0], [have_gio=yes], [have_gio=no]) PKG_CHECK_MODULES([GEOCLUE], [geoclue], [have_geoclue=yes], [have_geoclue=no]) AC_CHECK_HEADER([windows.h], [have_windows_h=yes], [have_windows_h=no]) @@ -153,6 +154,30 @@ AS_IF([test "x$enable_geoclue" != xno], [ ]) AM_CONDITIONAL([ENABLE_GEOCLUE], [test "x$enable_geoclue" = xyes]) +# Check DBus service +AC_MSG_CHECKING([whether to enable DBus service]) +AC_ARG_ENABLE([dbus], [AC_HELP_STRING([--enable-dbus], + [enable DBus service])], + [enable_dbus=$enableval],[enable_dbus=no]) +AS_IF([test "x$enable_dbus" != xno], [ + AS_IF([test "x$have_glib" = xyes -a "x$have_gio" = xyes -a "x$have_geoclue" = xyes], [ + AC_DEFINE([ENABLE_DBUS], 1, + [Define to 1 to enable DBus service]) + AC_MSG_RESULT([yes]) + enable_dbus=yes + ], [ + AC_MSG_RESULT([missing dependencies]) + AS_IF([test "x$enable_dbus" = xyes], [ + AC_MSG_ERROR([missing dependencies for DBus service]) + ]) + enable_dbus=no + ]) +], [ + AC_MSG_RESULT([no]) + enable_dbus=no +]) +AM_CONDITIONAL([ENABLE_DBUS], [test "x$enable_dbus" = xyes]) + # Check for GUI status icon AC_MSG_CHECKING([whether to enable GUI status icon]) @@ -245,6 +270,8 @@ echo " Location providers: Geoclue: ${enable_geoclue} + DBus service: ${enable_dbus} + GUI: ${enable_gui} Ubuntu icons: ${enable_ubuntu} systemd units: ${enable_systemd} ${systemduserunitdir} diff --git a/contrib/redshift.spec.in b/contrib/redshift.spec.in index 312d9ac1..154ee50d 100644 --- a/contrib/redshift.spec.in +++ b/contrib/redshift.spec.in @@ -12,6 +12,7 @@ BuildRequires: libXxf86vm-devel BuildRequires: libxcb-devel BuildRequires: geoclue-devel BuildRequires: systemd +BuildRequires: glib2 >= 2.26.0 %description Redshift adjusts the color temperature of your screen according to your @@ -44,7 +45,7 @@ temperature adjustment program. %setup -q %build -%configure --enable-gui --enable-geoclue --enable-randr --enable-vidmode --with-systemduserunitdir=%{_userunitdir} +%configure --libexecdir=%{_libexecdir}/%{name} --enable-gui --enable-geoclue --enable-randr --enable-vidmode --enable-dbus --with-systemduserunitdir=%{_userunitdir} make %{?_smp_mflags} V=1 %install @@ -69,8 +70,10 @@ gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : %defattr(-,root,root,-) %doc COPYING NEWS README README-colorramp %{_bindir}/redshift +%{_libexecdir}/%{name}/redshift-dbus %{_mandir}/man1/* %{_userunitdir}/* +%{_datadir}/dbus-1/services/dk.jonls.redshift.Redshift.service %files -n %{name}-gtk %defattr(-,root,root,-) diff --git a/data/dbus-1/services/dk.jonls.redshift.Redshift.service.in b/data/dbus-1/services/dk.jonls.redshift.Redshift.service.in new file mode 100644 index 00000000..b4fcc37a --- /dev/null +++ b/data/dbus-1/services/dk.jonls.redshift.Redshift.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=dk.jonls.redshift.Redshift +Exec=@libexecdir@/redshift-dbus diff --git a/src/Makefile.am b/src/Makefile.am index 37a03080..e36d54d8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -8,6 +8,11 @@ AM_CPPFLAGS = -DLOCALEDIR=\"$(localedir)\" # redshift Program bin_PROGRAMS = redshift +# redshift-dbus Program +if ENABLE_DBUS +libexec_PROGRAMS = redshift-dbus +endif + redshift_SOURCES = \ redshift.c redshift.h \ colorramp.c colorramp.h \ @@ -17,6 +22,12 @@ redshift_SOURCES = \ systemtime.c systemtime.h \ gamma-dummy.c gamma-dummy.h +redshift_dbus_SOURCES = \ + redshift-dbus.c \ + colorramp.c colorramp.h \ + solar.c solar.h \ + gamma-dummy.c gamma-dummy.h + EXTRA_redshift_SOURCES = \ gamma-drm.c gamma-drm.h \ gamma-randr.c gamma-randr.h \ @@ -24,31 +35,53 @@ EXTRA_redshift_SOURCES = \ gamma-w32gdi.c gamma-w32gdi.h \ location-geoclue.c location-geoclue.h -AM_CFLAGS = -redshift_LDADD = @LIBINTL@ + EXTRA_DIST = + +redshift_CFLAGS = +redshift_LDADD = @LIBINTL@ +redshift_dbus_CFLAGS = \ + $(GLIB_CFLAGS) $(GDBUS_CFLAGS) \ + $(GEOCLUE_CFLAGS) +redshift_dbus_LDADD = \ + $(GLIB_LIBS) $(GDBUS_LIBS) \ + $(GEOCLUE_LIBS) + + if ENABLE_DRM redshift_SOURCES += gamma-drm.c gamma-drm.h -AM_CFLAGS += $(DRM_CFLAGS) +redshift_CFLAGS += $(DRM_CFLAGS) redshift_LDADD += \ $(DRM_LIBS) $(DRM_CFLAGS) endif if ENABLE_RANDR redshift_SOURCES += gamma-randr.c gamma-randr.h -AM_CFLAGS += $(XCB_CFLAGS) $(XCB_RANDR_CFLAGS) +redshift_CFLAGS += $(XCB_CFLAGS) $(XCB_RANDR_CFLAGS) redshift_LDADD += \ $(XCB_LIBS) $(XCB_CFLAGS) \ $(XCB_RANDR_LIBS) $(XCB_RANDR_CFLAGS) + +redshift_dbus_SOURCES += gamma-randr.c gamma-randr.h +redshift_dbus_CFLAGS += $(XCB_CFLAGS) $(XCB_RANDR_CFLAGS) +redshift_dbus_LDADD += \ + $(XCB_LIBS) $(XCB_CFLAGS) \ + $(XCB_RANDR_LIBS) $(XCB_RANDR_CFLAGS) endif if ENABLE_VIDMODE redshift_SOURCES += gamma-vidmode.c gamma-vidmode.h -AM_CFLAGS += $(X11_CFLAGS) $(XF86VM_CFLAGS) +redshift_CFLAGS += $(X11_CFLAGS) $(XF86VM_CFLAGS) redshift_LDADD += \ $(X11_LIBS) $(X11_CFLAGS) \ $(XF86VM_LIBS) $(XF86VM_CFLAGS) + +redshift_dbus_SOURCES += gamma-vidmode.c gamma-vidmode.h +redshift_dbus_CFLAGS += $(X11_CFLAGS) $(XF86VM_CFLAGS) +redshift_dbus_LDADD += \ + $(X11_LIBS) $(X11_CFLAGS) \ + $(XF86VM_LIBS) $(XF86VM_CFLAGS) endif if ENABLE_WINGDI @@ -59,10 +92,7 @@ endif if ENABLE_GEOCLUE redshift_SOURCES += location-geoclue.c location-geoclue.h -AM_CFLAGS += \ - $(GEOCLUE_CFLAGS) $(GEOCLUE_LIBS) \ - $(GLIB_CFLAGS) $(GLIB_LIBS) +redshift_CFLAGS += $(GEOCLUE_CFLAGS) redshift_LDADD += \ $(GEOCLUE_LIBS) $(GEOCLUE_CFLAGS) - $(GLIB_LIBS) $(GLIB_CFLAGS) endif diff --git a/src/redshift-dbus.c b/src/redshift-dbus.c new file mode 100644 index 00000000..cab9ed5a --- /dev/null +++ b/src/redshift-dbus.c @@ -0,0 +1,1156 @@ +/* redshift-dbus.c -- DBus server for Redshift + This file is part of Redshift. + + Redshift is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Redshift is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Redshift. If not, see . + + Copyright (c) 2013 Jon Lund Steffensen +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include + +#include +#include +#include +#include + +#include "redshift.h" +#include "solar.h" + + +#ifdef ENABLE_RANDR +# include "gamma-randr.h" +#endif + +#ifdef ENABLE_VIDMODE +# include "gamma-vidmode.h" +#endif + +#include "gamma-dummy.h" + + +/* Union of state data for gamma adjustment methods */ +union { +#ifdef ENABLE_RANDR + randr_state_t randr; +#endif +#ifdef ENABLE_VIDMODE + vidmode_state_t vidmode; +#endif +} gamma_state; + + +/* Gamma adjustment method structs */ +static const gamma_method_t gamma_methods[] = { +#ifdef ENABLE_RANDR + { + "randr", 1, + (gamma_method_init_func *)randr_init, + (gamma_method_start_func *)randr_start, + (gamma_method_free_func *)randr_free, + (gamma_method_print_help_func *)randr_print_help, + (gamma_method_set_option_func *)randr_set_option, + (gamma_method_restore_func *)randr_restore, + (gamma_method_set_temperature_func *)randr_set_temperature + }, +#endif +#ifdef ENABLE_VIDMODE + { + "vidmode", 1, + (gamma_method_init_func *)vidmode_init, + (gamma_method_start_func *)vidmode_start, + (gamma_method_free_func *)vidmode_free, + (gamma_method_print_help_func *)vidmode_print_help, + (gamma_method_set_option_func *)vidmode_set_option, + (gamma_method_restore_func *)vidmode_restore, + (gamma_method_set_temperature_func *)vidmode_set_temperature + }, +#endif + { + "dummy", 0, + (gamma_method_init_func *)gamma_dummy_init, + (gamma_method_start_func *)gamma_dummy_start, + (gamma_method_free_func *)gamma_dummy_free, + (gamma_method_print_help_func *)gamma_dummy_print_help, + (gamma_method_set_option_func *)gamma_dummy_set_option, + (gamma_method_restore_func *)gamma_dummy_restore, + (gamma_method_set_temperature_func *)gamma_dummy_set_temperature + }, + { NULL } +}; + +static const gamma_method_t *current_method = NULL; + + +/* DBus names */ +#define REDSHIFT_BUS_NAME "dk.jonls.redshift.Redshift" +#define REDSHIFT_OBJECT_PATH "/dk/jonls/redshift/Redshift" +#define REDSHIFT_INTERFACE_NAME "dk.jonls.redshift.Redshift" + +/* Parameter bounds */ +#define LAT_MIN -90.0 +#define LAT_MAX 90.0 +#define LON_MIN -180.0 +#define LON_MAX 180.0 +#define TEMP_MIN 1000 +#define TEMP_MAX 25000 + +/* The color temperature when no adjustment is applied. */ +#define TEMP_NEUTRAL 6500 + +/* Default values for parameters. */ +#define DEFAULT_DAY_TEMP TEMP_NEUTRAL +#define DEFAULT_NIGHT_TEMP 3500 +#define DEFAULT_BRIGHTNESS 1.0 +#define DEFAULT_GAMMA 1.0 + +/* Angular elevation of the sun at which the color temperature + transition period starts and ends (in degress). + Transition during twilight, and while the sun is lower than + 3.0 degrees above the horizon. */ +#define TRANSITION_LOW SOLAR_CIVIL_TWILIGHT_ELEV +#define TRANSITION_HIGH 3.0 + +/* Periods of day */ +typedef enum { + PERIOD_NONE = 0, + PERIOD_DAY, + PERIOD_NIGHT, + PERIOD_TRANSITION +} period_t; + +static const gchar *period_names[] = { + "None", "Day", "Night", "Transition" +}; + + +static GDBusNodeInfo *introspection_data = NULL; + +/* Cookies for programs wanting to interact though + the DBus service. */ +static GHashTable *cookies = NULL; +static gint32 next_cookie = 1; + +/* Set of clients inhibiting the program. */ +static GHashTable *inhibitors = NULL; +static gboolean inhibited = FALSE; + +/* Solar elevation */ +static gdouble elevation = 0.0; +static period_t period = PERIOD_NONE; + +/* Location */ +static gdouble latitude = 0.0; +static gdouble longitude = 0.0; + +/* Temperature bounds */ +static guint temp_day = DEFAULT_DAY_TEMP; +static guint temp_night = DEFAULT_NIGHT_TEMP; + +/* Current temperature */ +static guint temperature = 0; +static guint temp_now = TEMP_NEUTRAL; + +/* Short transition parameters */ +static guint trans_timer = 0; +static guint trans_temp_start = 0; +static guint trans_length = 0; +static guint trans_time = 0; + +/* Forced temperature: 2-layered, so that an external program + can drive the temperature, and at the same time an external + program can demo temperatures as part of the user interface + (priority mode). */ +#define FORCED_TEMP_LAYERS 2 +static gint32 forced_temp_cookie[FORCED_TEMP_LAYERS] = {0}; +static guint forced_temp[FORCED_TEMP_LAYERS] = {0}; + +/* Forced location */ +static gint32 forced_location_cookie = 0; +static gdouble forced_lat = 0.0; +static gdouble forced_lon = 0.0; + +/* Screen update timer */ +static guint screen_update_timer = 0; + +/* Geoclue proxy objects */ +static GDBusProxy *geoclue_manager = NULL; +static GDBusProxy *geoclue_client = NULL; + + +/* DBus service definition */ +static const gchar introspection_xml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + + +/* Update elevation from location and time. */ +static void +update_elevation() +{ + gdouble time = g_get_real_time() / 1000000.0; + + /* Check for forced location */ + gdouble lat = latitude; + gdouble lon = longitude; + if (forced_location_cookie != 0) { + lat = forced_lat; + lon = forced_lon; + } + + elevation = solar_elevation(time, lat, lon); + + g_print("Location: %f, %f\n", lat, lon); + g_print("Elevation: %f\n", elevation); +} + +/* Update temperature from elevation */ +static void +update_temperature() +{ + /* Calculate temperature according to elevation */ + if (elevation < TRANSITION_LOW) { + temperature = temp_night; + } else if (elevation < TRANSITION_HIGH) { + float a = (TRANSITION_LOW - elevation) / + (TRANSITION_LOW - TRANSITION_HIGH); + temperature = (1.0-a)*temp_night + a*temp_day; + } else { + temperature = temp_day; + } +} + + +/* Timer callback to update short transitions */ +static gboolean +short_transition_update_cb(gpointer data) +{ + trans_time += 1; + gfloat a = trans_time/(gfloat)trans_length; + guint temp = (1.0-a)*trans_temp_start + a*temperature; + + const gfloat gamma[] = {1.0, 1.0, 1.0}; + if (current_method != NULL) { + current_method->set_temperature(&gamma_state, temp, + 1.0, gamma); + } + temp_now = temp; + + if (trans_time >= trans_length) { + trans_time = 0; + trans_length = 0; + return FALSE; + } + + return TRUE; +} + +/* Timer callback to update the screen */ +static gboolean +screen_update_cb(gpointer data) +{ + GDBusConnection *conn = G_DBUS_CONNECTION(data); + + /* Update elevation from location */ + update_elevation(); + + gboolean prev_inhibit = inhibited; + guint prev_temp = temperature; + period_t prev_period = period; + + /* Calculate period */ + if (elevation < TRANSITION_LOW) { + period = PERIOD_NIGHT; + } else if (elevation < TRANSITION_HIGH) { + period = PERIOD_TRANSITION; + } else { + period = PERIOD_DAY; + } + + /* Check for inhibition */ + inhibited = g_hash_table_size(inhibitors) > 0; + + if (inhibited) { + temperature = TEMP_NEUTRAL; + } else { + /* Check for forced temperature */ + int forced_index = -1; + for (int i = FORCED_TEMP_LAYERS-1; i >= 0; i--) { + if (forced_temp_cookie[i] != 0) { + forced_index = i; + break; + } + } + + if (forced_index < 0) { + update_temperature(); + } else { + temperature = forced_temp[forced_index]; + } + } + + g_print("Temperature: %u\n", temperature); + + /* Signal if temperature has changed */ + if (prev_temp != temperature || + prev_inhibit != inhibited || + prev_period != period) { + GError *local_error = NULL; + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + if (prev_temp != temperature) { + g_variant_builder_add(builder, "{sv}", + "Temperature", g_variant_new_uint32(temperature)); + } + if (prev_inhibit != inhibited) { + g_variant_builder_add(builder, "{sv}", + "Inhibited", g_variant_new_boolean(inhibited)); + } + if (prev_period != period) { + const char *name = period_names[period]; + g_variant_builder_add(builder, "{sv}", + "Period", g_variant_new_string(name)); + } + + g_dbus_connection_emit_signal(conn, NULL, + REDSHIFT_OBJECT_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", + REDSHIFT_INTERFACE_NAME, + builder, + NULL), + &local_error); + g_assert_no_error(local_error); + } + + /* If the temperature difference is large enough, + make a nice transition. */ + if (abs(temperature - temp_now) > 25) { + if (trans_timer != 0) { + g_source_remove(trans_timer); + } + + g_print("Create short transition: %u -> %u\n", temp_now, temperature); + trans_temp_start = temp_now; + trans_length = 40 - trans_time; + trans_time = 0; + + trans_timer = g_timeout_add(100, short_transition_update_cb, NULL); + } else if (temperature != temp_now) { + const gfloat gamma[] = {1.0, 1.0, 1.0}; + if (current_method != NULL) { + current_method->set_temperature(&gamma_state, temperature, + 1.0, gamma); + } + temp_now = temperature; + } + + return TRUE; +} + +static void +screen_update_restart(GDBusConnection *conn) +{ + if (screen_update_timer != 0) { + g_source_remove(screen_update_timer); + } + screen_update_cb(conn); + screen_update_timer = g_timeout_add_seconds(5, screen_update_cb, conn); +} + + +/* Emit signal that current position changed */ +static void +emit_position_changed(GDBusConnection *conn, gdouble lat, gdouble lon) +{ + /* Signal change in location */ + GError *local_error = NULL; + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + g_variant_builder_add(builder, "{sv}", "CurrentLatitude", + g_variant_new_double(lat)); + g_variant_builder_add(builder, "{sv}", "CurrentLongitude", + g_variant_new_double(lon)); + + g_dbus_connection_emit_signal(conn, NULL, + REDSHIFT_OBJECT_PATH, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", + REDSHIFT_INTERFACE_NAME, + builder, + NULL), + &local_error); + g_assert_no_error(local_error); +} + + +/* DBus service functions */ +static void +handle_method_call(GDBusConnection *conn, + const gchar *sender, + const gchar *obj_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer data) +{ + if (g_strcmp0(method_name, "AcquireCookie") == 0) { + gint32 cookie = next_cookie++; + const gchar *program; + g_variant_get(parameters, "(&s)", &program); + + g_print("AcquireCookie for `%s'.\n", program); + + g_hash_table_insert(cookies, GINT_TO_POINTER(cookie), g_strdup(program)); + g_dbus_method_invocation_return_value(invocation, + g_variant_new("(i)", cookie)); + } else if (g_strcmp0(method_name, "ReleaseCookie") == 0) { + gint32 cookie; + g_variant_get(parameters, "(i)", &cookie); + + gchar *program = g_hash_table_lookup(cookies, GINT_TO_POINTER(cookie)); + if (program == NULL) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.UnknownCookie", + "Unknown cookie value"); + return; + } + + g_print("ReleaseCookie for `%s'.\n", program); + + /* Remove all rules enforced by program */ + gboolean found = FALSE; + found = g_hash_table_remove(inhibitors, GINT_TO_POINTER(cookie)); + + for (int i = 0; i < FORCED_TEMP_LAYERS; i++) { + if (forced_temp_cookie[i] == cookie) { + forced_temp_cookie[i] = 0; + found = TRUE; + } + } + + if (forced_location_cookie == cookie) { + forced_location_cookie = 0; + found = TRUE; + } + + if (found) screen_update_restart(conn); + + /* Remove from list of cookies */ + g_hash_table_remove(cookies, GINT_TO_POINTER(cookie)); + g_free(program); + + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "Inhibit") == 0) { + gint32 cookie; + g_variant_get(parameters, "(i)", &cookie); + + gchar *program = g_hash_table_lookup(cookies, GINT_TO_POINTER(cookie)); + if (program == NULL) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.UnknownCookie", + "Unknown cookie value"); + return; + } + + g_hash_table_add(inhibitors, GINT_TO_POINTER(cookie)); + + if (!inhibited) { + screen_update_restart(conn); + } + + g_print("Inhibit for `%s'.\n", program); + + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "Uninhibit") == 0) { + gint32 cookie; + g_variant_get(parameters, "(i)", &cookie); + + gchar *program = g_hash_table_lookup(cookies, GINT_TO_POINTER(cookie)); + if (program == NULL) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.UnknownCookie", + "Unknown cookie value"); + return; + } + + g_hash_table_remove(inhibitors, GINT_TO_POINTER(cookie)); + + if (inhibited && g_hash_table_size(inhibitors) == 0) { + screen_update_restart(conn); + } + + g_print("Uninhibit for `%s'.\n", program); + + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "EnforceTemperature") == 0) { + gint32 cookie; + guint32 temp; + gboolean priority; + g_variant_get(parameters, "(iub)", &cookie, &temp, &priority); + + gchar *program = g_hash_table_lookup(cookies, GINT_TO_POINTER(cookie)); + if (program == NULL) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.UnknownCookie", + "Unknown cookie value"); + return; + } + + int index = priority ? 1 : 0; + + if (forced_temp_cookie[index] != 0 && + forced_temp_cookie[index] != cookie) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.AlreadyEnforced", + "Another client is already enforcing temperature"); + return; + } + + /* Check parameter bounds */ + if (temp < TEMP_MIN || temp > TEMP_MAX) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.InvalidArgument", + "Temperature is invalid"); + return; + } + + /* Set forced location */ + forced_temp_cookie[index] = cookie; + forced_temp[index] = temp; + + screen_update_restart(conn); + + g_print("EnforceTemperature for `%s'.\n", program); + + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "UnenforceTemperature") == 0) { + gint32 cookie; + gboolean priority; + g_variant_get(parameters, "(ib)", &cookie, &priority); + + gchar *program = g_hash_table_lookup(cookies, GINT_TO_POINTER(cookie)); + if (program == NULL) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.UnknownCookie", + "Unknown cookie value"); + return; + } + + g_print("UnenforceTemperature for `%s'.\n", program); + + int index = priority ? 1 : 0; + + if (forced_temp_cookie[index] == cookie) { + forced_temp_cookie[index] = 0; + screen_update_restart(conn); + } + + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "EnforceLocation") == 0) { + gint32 cookie; + gdouble lat; + gdouble lon; + g_variant_get(parameters, "(idd)", &cookie, &lat, &lon); + + gchar *program = g_hash_table_lookup(cookies, GINT_TO_POINTER(cookie)); + if (program == NULL) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.UnknownCookie", + "Unknown cookie value"); + return; + } + + if (forced_location_cookie != 0 && + forced_location_cookie != cookie) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.AlreadyEnforced", + "Another client is already enforcing location"); + return; + } + + /* Check parameter bounds */ + if (lat < LAT_MIN || lat > LAT_MAX || + lon < LON_MIN || lon > LON_MAX) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.InvalidArgument", + "Location is invalid"); + return; + } + + /* Set forced location */ + forced_location_cookie = cookie; + forced_lat = lat; + forced_lon = lon; + + /* Signal change in location */ + emit_position_changed(conn, forced_lat, forced_lon); + + screen_update_restart(conn); + + g_print("EnforceLocation for `%s'.\n", program); + + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "UnenforceLocation") == 0) { + gint32 cookie; + g_variant_get(parameters, "(i)", &cookie); + + gchar *program = g_hash_table_lookup(cookies, GINT_TO_POINTER(cookie)); + if (program == NULL) { + g_dbus_method_invocation_return_dbus_error(invocation, + "dk.jonls.redshift.Redshift.UnknownCookie", + "Unknown cookie value"); + return; + } + + g_print("UnenforceLocation for `%s'.\n", program); + + if (forced_location_cookie == cookie) { + forced_location_cookie = 0; + screen_update_restart(conn); + + /* Signal change in location */ + emit_position_changed(conn, latitude, longitude); + } + + g_dbus_method_invocation_return_value(invocation, NULL); + } else if (g_strcmp0(method_name, "GetElevation") == 0) { + g_dbus_method_invocation_return_value(invocation, + g_variant_new("(d)", elevation)); + } +} + +static GVariant * +handle_get_property(GDBusConnection *conn, + const gchar *sender, + const gchar *obj_path, + const gchar *interface_name, + const gchar *prop_name, + GError **error, + gpointer data) +{ + GVariant *ret = NULL; + + if (g_strcmp0(prop_name, "Inhibited") == 0) { + ret = g_variant_new_boolean(inhibited); + } else if (g_strcmp0(prop_name, "Period") == 0) { + ret = g_variant_new_string(period_names[period]); + } else if (g_strcmp0(prop_name, "Temperature") == 0) { + ret = g_variant_new_uint32(temperature); + } else if (g_strcmp0(prop_name, "CurrentLatitude") == 0) { + gdouble lat = latitude; + if (forced_location_cookie != 0) lat = forced_lat; + ret = g_variant_new_double(lat); + } else if (g_strcmp0(prop_name, "CurrentLongitude") == 0) { + gdouble lon = longitude; + if (forced_location_cookie != 0) lon = forced_lon; + ret = g_variant_new_double(lon); + } else if (g_strcmp0(prop_name, "TemperatureDay") == 0) { + ret = g_variant_new_uint32(temp_day); + } else if (g_strcmp0(prop_name, "TemperatureNight") == 0) { + ret = g_variant_new_uint32(temp_night); + } + + return ret; +} + +static gboolean +handle_set_property(GDBusConnection *conn, + const gchar *sender, + const gchar *obj_path, + const gchar *interface_name, + const gchar *prop_name, + GVariant *value, + GError **error, + gpointer data) +{ + if (g_strcmp0(prop_name, "TemperatureDay") == 0) { + guint32 temp = g_variant_get_uint32(value); + + if (temp < TEMP_MIN || temp > TEMP_MAX) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Temperature out of bounds"); + } else { + temp_day = temp; + + screen_update_restart(conn); + + GError *local_error = NULL; + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + g_variant_builder_add(builder, "{sv}", "TemperatureDay", + g_variant_new_uint32(temp_day)); + + g_dbus_connection_emit_signal(conn, NULL, + obj_path, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", + interface_name, + builder, + NULL), + &local_error); + g_assert_no_error(local_error); + } + } else if (g_strcmp0(prop_name, "TemperatureNight") == 0) { + guint32 temp = g_variant_get_uint32(value); + + if (temp < TEMP_MIN || temp > TEMP_MAX) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Temperature out of bounds"); + } else { + temp_night = temp; + + screen_update_restart(conn); + + GError *local_error = NULL; + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + g_variant_builder_add(builder, "{sv}", "TemperatureNight", + g_variant_new_uint32(temp_night)); + + g_dbus_connection_emit_signal(conn, NULL, + obj_path, + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", + interface_name, + builder, + NULL), + &local_error); + g_assert_no_error(local_error); + } + } + + return *error == NULL; +} + + +/* Save position state */ +static void +save_position_state() +{ + /* Probably should use STATE directory when that + is standardized in the XDG spec. */ + char *path = g_build_filename(g_get_user_data_dir(), + "redshift", "position", NULL); + FILE *state = g_fopen(path, "w"); + if (state == NULL) { + /* Try to create the directory */ + char *path_dir = g_path_get_dirname(path); + gint r = g_mkdir_with_parents(path_dir, S_IRWXU); + if (r == 0) { + state = g_fopen(path, "w"); + } + g_free(path_dir); + } + + g_free(path); + + if (state != NULL) { + g_fprintf(state, "%f\n%f\n", latitude, longitude); + fclose(state); + } +} + +/* Restore saved position state */ +static void +restore_position_state() +{ + /* Probably should use STATE directory when that + is standardized in the XDG spec. */ + char *path = g_build_filename(g_get_user_data_dir(), + "redshift", "position", NULL); + FILE *state = g_fopen(path, "r"); + + g_free(path); + + if (state != NULL) { + gdouble lat, lon; + char buffer[64]; + + /* Read latitude */ + char *r = fgets(buffer, sizeof(buffer), state); + if (r == NULL) { + fclose(state); + return; + } + + lat = g_ascii_strtod(buffer, NULL); + + /* Read longitude */ + r = fgets(buffer, sizeof(buffer), state); + if (r == NULL) { + fclose(state); + return; + } + + lon = g_ascii_strtod(buffer, NULL); + + g_print("Restored position %.2f, %.2f\n", lat, lon); + + latitude = lat; + longitude = lon; + } +} + + +/* Handle position change callbacks */ +static void +geoclue_client_signal_cb(GDBusProxy *client, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer *data) +{ + GDBusConnection *conn = G_DBUS_CONNECTION(data); + + /* Only handle LocationUpdated signals */ + if (g_strcmp0(signal_name, "LocationUpdated") != 0) { + return; + } + + /* Obtain location path */ + const gchar *location_path; + g_variant_get_child(parameters, 1, "&o", &location_path); + + /* Obtain location */ + GError *error = NULL; + GDBusProxy *location = + g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.GeoClue2", + location_path, + "org.freedesktop.GeoClue2.Location", + NULL, &error); + if (location == NULL) { + g_printerr("Unable to obtain location: %s.\n", + error->message); + g_error_free(error); + return; + } + + /* Read location properties */ + GVariant *lat_v = g_dbus_proxy_get_cached_property(location, + "Latitude"); + gdouble lat = g_variant_get_double(lat_v); + + GVariant *lon_v = g_dbus_proxy_get_cached_property(location, + "Longitude"); + gdouble lon = g_variant_get_double(lon_v); + + /* Save position */ + latitude = lat; + longitude = lon; + save_position_state(); + + g_print("Position from Geoclue: %.2f, %.2f\n", lat, lon); + + if (forced_location_cookie == 0) { + emit_position_changed(conn, latitude, longitude); + } +} + +/* Initialize Geoclue position client */ +static int +init_geoclue_position(GDBusConnection *conn) +{ + /* Obtain Geoclue Manager */ + GError *error = NULL; + geoclue_manager = + g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.GeoClue2", + "/org/freedesktop/GeoClue2/Manager", + "org.freedesktop.GeoClue2.Manager", + NULL, &error); + if (geoclue_manager == NULL) { + g_printerr("Unable to obtain Geoclue Manager: %s.\n", + error->message); + g_error_free(error); + return -1; + } + + /* Obtain Geoclue Client path */ + error = NULL; + GVariant *client_path_v = + g_dbus_proxy_call_sync(geoclue_manager, + "GetClient", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); + if (client_path_v == NULL) { + g_printerr("Unable to obtain Geoclue client path: %s.\n", + error->message); + g_error_free(error); + g_object_unref(geoclue_manager); + return -1; + } + + const gchar *client_path; + g_variant_get(client_path_v, "(&o)", &client_path); + + /* Obtain GeoClue client */ + error = NULL; + geoclue_client = + g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.GeoClue2", + client_path, + "org.freedesktop.GeoClue2.Client", + NULL, &error); + if (geoclue_client == NULL) { + g_printerr("Unable to obtain Geoclue Client: %s.\n", + error->message); + g_error_free(error); + g_variant_unref(client_path_v); + g_object_unref(geoclue_manager); + return -1; + } + + g_variant_unref(client_path_v); + + /* Set distance threshold */ + error = NULL; + GVariant *ret_v = + g_dbus_proxy_call_sync(geoclue_client, + "org.freedesktop.DBus.Properties.Set", + g_variant_new("(ssv)", + "org.freedesktop.GeoClue2.Client", + "DistanceThreshold", + g_variant_new("u", 10000)), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); + if (ret_v == NULL) { + g_printerr("Unable to set distance threshold: %s.\n", + error->message); + g_error_free(error); + g_object_unref(geoclue_client); + g_object_unref(geoclue_manager); + return -1; + } + + g_variant_unref(ret_v); + + /* Attach signal callback to client */ + g_signal_connect(geoclue_client, "g-signal", + G_CALLBACK(geoclue_client_signal_cb), + conn); + + /* Start Geoclue client */ + error = NULL; + ret_v = g_dbus_proxy_call_sync(geoclue_client, + "Start", + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); + if (ret_v == NULL) { + g_printerr("Unable to start Geoclue client: %s.\n", + error->message); + g_error_free(error); + g_object_unref(geoclue_client); + g_object_unref(geoclue_manager); + return -1; + } + + g_variant_unref(ret_v); + + return 0; +} + + +static const GDBusInterfaceVTable interface_vtable = { + handle_method_call, + handle_get_property, + handle_set_property +}; + + +static void +on_bus_acquired(GDBusConnection *conn, + const gchar *name, + gpointer data) +{ + g_printerr("Bus acquired: `%s'.\n", name); + + guint registration_id = g_dbus_connection_register_object(conn, + REDSHIFT_OBJECT_PATH, + introspection_data->interfaces[0], + &interface_vtable, + NULL, NULL, + NULL); + g_assert(registration_id > 0); + + /* Initialize Geoclue position provider */ + int r = init_geoclue_position(conn); + if (r < 0) exit(EXIT_FAILURE); + + /* Start screen update timer */ + screen_update_restart(conn); +} + +static void +on_name_acquired(GDBusConnection *conn, + const gchar *name, + gpointer data) +{ + g_printerr("Name acquired: `%s'.\n", name); +} + +static void +on_name_lost(GDBusConnection *conn, + const gchar *name, + gpointer data) +{ + g_printerr("Name lost: `%s'.\n", name); + exit(EXIT_FAILURE); +} + + +/* Handle termination signal */ +static gboolean +term_signal_cb(gpointer data) +{ + GMainLoop *loop = (GMainLoop *)data; + g_main_loop_quit(loop); + return TRUE; +} + + +int +main(int argc, char *argv[]) +{ +#if !GLIB_CHECK_VERSION(2, 35, 0) + g_type_init(); +#endif + + /* Restore position state from last run */ + restore_position_state(); + + /* Create hash table for cookies */ + cookies = g_hash_table_new(NULL, NULL); + + /* Create hash table for inhibitors (set) */ + inhibitors = g_hash_table_new(NULL, NULL); + + /* Setup gamma method */ + for (int i = 0; gamma_methods[i].name != NULL; i++) { + const gamma_method_t *m = &gamma_methods[i]; + + int r = m->init(&gamma_state); + if (r < 0) continue; + + r = m->start(&gamma_state); + if (r < 0) continue; + + current_method = m; + g_print("Using method `%s'.\n", current_method->name); + break; + } + + /* Build node info from XML */ + introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, + NULL); + g_assert(introspection_data != NULL); + + /* Obtain DBus bus name */ + GBusNameOwnerFlags flags = + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE; + guint owner_id = g_bus_own_name(G_BUS_TYPE_SESSION, + REDSHIFT_BUS_NAME, + flags, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + /* Create main loop */ + GMainLoop *mainloop = g_main_loop_new(NULL, FALSE); + + /* Attach signal handler for termination */ + g_unix_signal_add(SIGTERM, term_signal_cb, mainloop); + g_unix_signal_add(SIGINT, term_signal_cb, mainloop); + + /* Start main loop */ + g_main_loop_run(mainloop); + + /* Clean up */ + g_bus_unown_name(owner_id); + g_object_unref(geoclue_client); + g_object_unref(geoclue_manager); + + g_print("Restoring gamma ramps.\n"); + + /* Restore gamma ramps */ + if (current_method != NULL) { + current_method->restore(&gamma_state); + current_method->free(&gamma_state); + } + + return 0; +} diff --git a/src/redshift-gtk/statusicon.py b/src/redshift-gtk/statusicon.py index 2efbc074..7ca89b7a 100644 --- a/src/redshift-gtk/statusicon.py +++ b/src/redshift-gtk/statusicon.py @@ -24,11 +24,13 @@ ''' import sys, os -import signal, fcntl +import signal import re import gettext -from gi.repository import Gdk, Gtk, GLib +from gi.repository import Gdk, Gtk, GLib, GObject +from gi.repository import Gio + try: from gi.repository import AppIndicator3 as appindicator except ImportError: @@ -40,27 +42,93 @@ _ = gettext.gettext -def sigterm_handler(signal, frame): - sys.exit() +class RedshiftController(GObject.GObject): + __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)) + } + + def __init__(self, name): + GObject.GObject.__init__(self) + + # Connect to Redshift DBus service + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + self._redshift = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, + 'dk.jonls.redshift.Redshift', + '/dk/jonls/redshift/Redshift', + 'dk.jonls.redshift.Redshift', None) + self._redshift_props = Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, + 'dk.jonls.redshift.Redshift', + '/dk/jonls/redshift/Redshift', + 'org.freedesktop.DBus.Properties', None) + + self._name = name + self._cookie = None + + # Setup signals to property changes + self._redshift.connect('g-properties-changed', self._property_change_cb) + + def _property_change_cb(self, proxy, props, invalid, data=None): + # Cast to python dict as 'in' keyword is broken for GLib dict + props = dict(props) + if 'Inhibited' in props: + self.emit('inhibit-changed', props['Inhibited']) + if 'Temperature' in props: + self.emit('temperature-changed', props['Temperature']) + if 'Period' in props: + self.emit('period-changed', props['Period']) + if 'CurrentLatitude' in props and 'CurrentLongitude' in props: + self.emit('location-changed', props['CurrentLatitude'], props['CurrentLongitude']) + + @property + def cookie(self): + if self._cookie is None: + self._cookie = self._redshift.AcquireCookie('(s)', self._name) + return self._cookie + + def release_cookie(self): + if self._cookie is not None: + self._redshift.ReleaseCookie('(i)', self._cookie) + self._cookie = None + + def __del__(self): + self.release_cookie() + + @property + def elevation(self): + return self._redshift.GetElevation() + + @property + def inhibited(self): + return self._redshift_props.Get('(ss)', 'dk.jonls.redshift.Redshift', 'Inhibited') + + @property + def temperature(self): + return self._redshift_props.Get('(ss)', 'dk.jonls.redshift.Redshift', 'Temperature') + + @property + def period(self): + return self._redshift_props.Get('(ss)', 'dk.jonls.redshift.Redshift', 'Period') + + @property + def location(self): + lat = self._redshift_props.Get('(ss)', 'dk.jonls.redshift.Redshift', 'CurrentLatitude') + lon = self._redshift_props.Get('(ss)', 'dk.jonls.redshift.Redshift', 'CurrentLongitude') + return (lat, lon) + + def set_inhibit(self, inhibit): + if inhibit: + self._redshift.Inhibit('(i)', self.cookie) + else: + self._redshift.Uninhibit('(i)', self.cookie) 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) + def __init__(self): + # Initialize controller + self._controller = RedshiftController('redshift-gtk') if appindicator: # Create indicator @@ -149,6 +217,18 @@ def __init__(self, args=[]): self.info_dialog.connect('response', self.response_info_cb) + # 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('location-changed', self.location_change_cb) + + # Set info box text + self.change_inhibited(self._controller.inhibited) + self.change_period(self._controller.period) + self.change_temperature(self._controller.temperature) + self.change_location(self._controller.location) + if appindicator: self.status_menu.show_all() @@ -163,54 +243,24 @@ def __init__(self, args=[]): # 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(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)) - # Notify desktop that startup is complete Gdk.notify_startup_complete() - 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.items()], - 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. + # Inhibit + self._controller.set_inhibit(True) 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() + self._controller.set_inhibit(False) def popup_menu_cb(self, widget, button, time, data=None): self.status_menu.show_all() @@ -219,13 +269,14 @@ def popup_menu_cb(self, widget, button, time, data=None): def toggle_cb(self, widget, data=None): self.remove_suspend_timer() - self.child_toggle_status() + self._controller.set_inhibit(not self._controller.inhibited) def toggle_item_cb(self, widget, data=None): # Only toggle if a change from current state was requested - if self.is_enabled() != widget.get_active(): + active = not self._controller.inhibited + if active != widget.get_active(): self.remove_suspend_timer() - self.child_toggle_status() + self._controller.set_inhibit(not self._controller.inhibited) # Info dialog callbacks def show_info_cb(self, widget, data=None): @@ -237,41 +288,46 @@ def response_info_cb(self, widget, data=None): def update_status_icon(self): # Update status icon if appindicator: - if self.is_enabled(): + if not self._controller.inhibited: self.indicator.set_icon('redshift-status-on') else: self.indicator.set_icon('redshift-status-off') else: - if self.is_enabled(): + 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') - # Status update functions. Called when child process indicates a - # change in state. - def change_status(self, status): - self._status = status + # State update functions + def inhibit_change_cb(self, controller, inhibit): + self.change_inhibited(inhibit) + + def period_change_cb(self, controller, period): + self.change_period(period) + + def temperature_change_cb(self, controller, temperature): + self.change_temperature(temperature) + + def location_change_cb(self, controller, lat, lon): + self.change_location((lat, lon)) + + + # Update interface + def change_inhibited(self, inhibited): self.update_status_icon() - self.toggle_item.set_active(self.is_enabled()) - self.status_label.set_markup('{}: {}'.format(_('Status'), - _('Enabled') if status else _('Disabled'))) + self.toggle_item.set_active(not inhibited) + self.status_label.set_markup(_('Status: {}').format(_('Disabled') if inhibited else _('Enabled'))) def change_temperature(self, temperature): - self._temperature = temperature - self.temperature_label.set_markup('{}: {}K'.format(_('Color temperature'), temperature)) + self.temperature_label.set_markup(_('Color temperature: {}K').format(temperature)) def change_period(self, period): - self._period = period - self.period_label.set_markup('{}: {}'.format(_('Period'), period)) + self.period_label.set_markup(_('Period: {}').format(period)) def change_location(self, location): - self._location = location - self.location_label.set_markup('{}: {}, {}'.format(_('Location'), *location)) - + self.location_label.set_markup(_('Location: {}, {}').format(*location)) - def is_enabled(self): - return self._status def autostart_cb(self, widget, data=None): utils.set_autostart(widget.get_active()) @@ -282,67 +338,22 @@ def destroy_cb(self, widget, data=None): 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.rstrip('K'), 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).decode('utf-8') - - # Split input at line break - while True: - first, sep, last = ib.buf.partition('\n') - if sep == '': - break - 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 sigterm_handler(signal, frame): + sys.exit(0) def run(): utils.setproctitle('redshift-gtk') + # Install TERM signal handler + signal.signal(signal.SIGTERM, sigterm_handler) + # 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 - s.termwait() + s = RedshiftStatusIcon() + + # Run main loop + Gtk.main()