diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 37 | ||||
-rw-r--r-- | src/gamma-quartz.c | 9 | ||||
-rw-r--r-- | src/gamma-randr.c | 2 | ||||
-rw-r--r-- | src/gamma-vidmode.c | 2 | ||||
-rw-r--r-- | src/gamma-w32gdi.c | 22 | ||||
-rw-r--r-- | src/location-corelocation.h | 34 | ||||
-rw-r--r-- | src/location-corelocation.m | 245 | ||||
-rw-r--r-- | src/location-geoclue.c | 218 | ||||
-rw-r--r-- | src/location-geoclue.h | 46 | ||||
-rw-r--r-- | src/location-geoclue2.c | 347 | ||||
-rw-r--r-- | src/location-geoclue2.h | 29 | ||||
-rw-r--r-- | src/location-manual.c | 15 | ||||
-rw-r--r-- | src/location-manual.h | 7 | ||||
-rw-r--r-- | src/pipeutils.c | 98 | ||||
-rw-r--r-- | src/pipeutils.h | 28 | ||||
-rw-r--r-- | src/redshift-gtk/Makefile.am | 5 | ||||
-rw-r--r-- | src/redshift-gtk/controller.py | 227 | ||||
-rw-r--r-- | src/redshift-gtk/statusicon.py | 359 | ||||
-rw-r--r-- | src/redshift-gtk/utils.py | 34 | ||||
-rw-r--r-- | src/redshift.c | 889 | ||||
-rw-r--r-- | src/redshift.h | 9 | ||||
-rw-r--r-- | src/windows/appicon.rc | 1 | ||||
-rw-r--r-- | src/windows/redshift.ico | bin | 0 -> 87891 bytes | |||
-rw-r--r-- | src/windows/versioninfo.rc | 20 |
24 files changed, 1664 insertions, 1019 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 318fc2c..99c8a2e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -9,15 +9,16 @@ AM_CPPFLAGS = -DLOCALEDIR=\"$(localedir)\" bin_PROGRAMS = redshift redshift_SOURCES = \ - redshift.c redshift.h \ - signals.c signals.h \ colorramp.c colorramp.h \ config-ini.c config-ini.h \ + gamma-dummy.c gamma-dummy.h \ + hooks.c hooks.h \ location-manual.c location-manual.h \ + pipeutils.c pipeutils.h \ + redshift.c redshift.h \ + signals.c signals.h \ solar.c solar.h \ - systemtime.c systemtime.h \ - hooks.c hooks.h \ - gamma-dummy.c gamma-dummy.h + systemtime.c systemtime.h EXTRA_redshift_SOURCES = \ gamma-drm.c gamma-drm.h \ @@ -25,11 +26,14 @@ EXTRA_redshift_SOURCES = \ gamma-vidmode.c gamma-vidmode.h \ gamma-quartz.c gamma-quartz.h \ gamma-w32gdi.c gamma-w32gdi.h \ - location-geoclue.c location-geoclue.h + location-geoclue2.c location-geoclue2.h \ + location-corelocation.m location-corelocation.h \ + windows/appicon.rc \ + windows/versioninfo.rc AM_CFLAGS = redshift_LDADD = @LIBINTL@ -EXTRA_DIST = +EXTRA_DIST = windows/redshift.ico if ENABLE_DRM redshift_SOURCES += gamma-drm.c gamma-drm.h @@ -67,16 +71,6 @@ redshift_LDADD += -lgdi32 endif -if ENABLE_GEOCLUE -redshift_SOURCES += location-geoclue.c location-geoclue.h -AM_CFLAGS += \ - $(GEOCLUE_CFLAGS) $(GEOCLUE_LIBS) \ - $(GLIB_CFLAGS) $(GLIB_LIBS) -redshift_LDADD += \ - $(GEOCLUE_LIBS) $(GEOCLUE_CFLAGS) - $(GLIB_LIBS) $(GLIB_CFLAGS) -endif - if ENABLE_GEOCLUE2 redshift_SOURCES += location-geoclue2.c location-geoclue2.h AM_CFLAGS += \ @@ -99,3 +93,12 @@ liblocation_corelocation_la_LIBADD = \ $(CORELOCATION_CFLAGS) $(CORELOCATION_LIBS) redshift_LDADD += liblocation-corelocation.la endif + + +# Windows resources +if ENABLE_WINDOWS_RESOURCE +redshift_SOURCES += windows/appicon.rc windows/versioninfo.rc +endif + +.rc.o: + $(AM_V_GEN)$(WINDRES) -I$(top_builddir) -i $< -o $@ diff --git a/src/gamma-quartz.c b/src/gamma-quartz.c index 6691c91..879da21 100644 --- a/src/gamma-quartz.c +++ b/src/gamma-quartz.c @@ -40,7 +40,7 @@ int quartz_init(quartz_state_t *state) { - state->preserve = 0; + state->preserve = 1; state->displays = NULL; return 0; @@ -177,10 +177,11 @@ quartz_set_option(quartz_state_t *state, const char *key, const char *value) } static void -quartz_set_temperature_for_display(quartz_state_t *state, int display, +quartz_set_temperature_for_display(quartz_state_t *state, int display_index, const color_setting_t *setting) { - uint32_t ramp_size = state->displays[display].ramp_size; + CGDirectDisplayID display = state->displays[display_index].display; + uint32_t ramp_size = state->displays[display_index].ramp_size; /* Create new gamma ramps */ float *gamma_ramps = malloc(3*ramp_size*sizeof(float)); @@ -195,7 +196,7 @@ quartz_set_temperature_for_display(quartz_state_t *state, int display, if (state->preserve) { /* Initialize gamma ramps from saved state */ - memcpy(gamma_ramps, state->displays[display].saved_ramps, + memcpy(gamma_ramps, state->displays[display_index].saved_ramps, 3*ramp_size*sizeof(float)); } else { /* Initialize gamma ramps to pure state */ diff --git a/src/gamma-randr.c b/src/gamma-randr.c index 901c0db..6e0fd00 100644 --- a/src/gamma-randr.c +++ b/src/gamma-randr.c @@ -53,7 +53,7 @@ randr_init(randr_state_t *state) state->crtc_count = 0; state->crtcs = NULL; - state->preserve = 0; + state->preserve = 1; xcb_generic_error_t *error; diff --git a/src/gamma-vidmode.c b/src/gamma-vidmode.c index c9682d2..e664f80 100644 --- a/src/gamma-vidmode.c +++ b/src/gamma-vidmode.c @@ -43,7 +43,7 @@ vidmode_init(vidmode_state_t *state) state->screen_num = -1; state->saved_ramps = NULL; - state->preserve = 0; + state->preserve = 1; /* Open display */ state->display = XOpenDisplay(NULL); diff --git a/src/gamma-w32gdi.c b/src/gamma-w32gdi.c index c518fe6..3d67331 100644 --- a/src/gamma-w32gdi.c +++ b/src/gamma-w32gdi.c @@ -37,13 +37,14 @@ #include "colorramp.h" #define GAMMA_RAMP_SIZE 256 +#define MAX_ATTEMPTS 10 int w32gdi_init(w32gdi_state_t *state) { state->saved_ramps = NULL; - state->preserve = 0; + state->preserve = 1; return 0; } @@ -136,7 +137,13 @@ w32gdi_restore(w32gdi_state_t *state) } /* Restore gamma ramps */ - BOOL r = SetDeviceGammaRamp(hDC, state->saved_ramps); + BOOL r = FALSE; + for (int i = 0; i < MAX_ATTEMPTS && !r; i++) { + /* We retry a few times before giving up because some + buggy drivers fail on the first invocation of + SetDeviceGammaRamp just to succeed on the second. */ + r = SetDeviceGammaRamp(hDC, state->saved_ramps); + } if (!r) fputs(_("Unable to restore gamma ramps.\n"), stderr); /* Release device context */ @@ -187,11 +194,14 @@ w32gdi_set_temperature(w32gdi_state_t *state, setting); /* Set new gamma ramps */ - r = SetDeviceGammaRamp(hDC, gamma_ramps); + r = FALSE; + for (int i = 0; i < MAX_ATTEMPTS && !r; i++) { + /* We retry a few times before giving up because some + buggy drivers fail on the first invocation of + SetDeviceGammaRamp just to succeed on the second. */ + r = SetDeviceGammaRamp(hDC, gamma_ramps); + } if (!r) { - /* TODO it happens that SetDeviceGammaRamp returns FALSE on - occasions where the adjustment seems to be successful. - Does this only happen with multiple monitors connected? */ fputs(_("Unable to set gamma ramps.\n"), stderr); free(gamma_ramps); ReleaseDC(NULL, hDC); diff --git a/src/location-corelocation.h b/src/location-corelocation.h index 4b74382..ae1feeb 100644 --- a/src/location-corelocation.h +++ b/src/location-corelocation.h @@ -14,7 +14,7 @@ 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) 2014 Jon Lund Steffense <jonlst@gmail.com> + Copyright (c) 2014-2017 Jon Lund Steffense <jonlst@gmail.com> */ #ifndef REDSHIFT_LOCATION_CORELOCATION_H @@ -24,17 +24,33 @@ #include "redshift.h" +typedef struct location_corelocation_private location_corelocation_private_t; -int location_corelocation_init(void *state); -int location_corelocation_start(void *state); -void location_corelocation_free(void *state); +typedef struct { + location_corelocation_private_t *private; + int pipe_fd_read; + int pipe_fd_write; + int available; + int error; + float latitude; + float longitude; +} location_corelocation_state_t; -void location_corelocation_print_help(FILE *f); -int location_corelocation_set_option(void *state, - const char *key, const char *value); -int location_corelocation_get_location(void *state, - location_t *location); +int location_corelocation_init(location_corelocation_state_t *state); +int location_corelocation_start(location_corelocation_state_t *state); +void location_corelocation_free(location_corelocation_state_t *state); + +void location_corelocation_print_help(FILE *f); +int location_corelocation_set_option( + location_corelocation_state_t *state, + const char *key, const char *value); + +int location_corelocation_get_fd( + location_corelocation_state_t *state); +int location_corelocation_handle( + location_corelocation_state_t *state, + location_t *location, int *available); #endif /* ! REDSHIFT_LOCATION_CORELOCATION_H */ diff --git a/src/location-corelocation.m b/src/location-corelocation.m index 2f1768d..5150839 100644 --- a/src/location-corelocation.m +++ b/src/location-corelocation.m @@ -14,7 +14,7 @@ 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) 2014 Jon Lund Steffensen <jonlst@gmail.com> + Copyright (c) 2014-2017 Jon Lund Steffensen <jonlst@gmail.com> */ #ifdef HAVE_CONFIG_H @@ -25,9 +25,11 @@ #import <CoreLocation/CoreLocation.h> #include "location-corelocation.h" +#include "pipeutils.h" #include "redshift.h" #include <stdio.h> +#include <unistd.h> #ifdef ENABLE_NLS # include <libintl.h> @@ -37,128 +39,229 @@ #endif -@interface Delegate : NSObject <CLLocationManagerDelegate> +struct location_corelocation_private { + NSThread *thread; + NSLock *lock; +}; + + +@interface LocationDelegate : NSObject <CLLocationManagerDelegate> @property (strong, nonatomic) CLLocationManager *locationManager; -@property (nonatomic) BOOL success; -@property (nonatomic) float latitude; -@property (nonatomic) float longitude; +@property (nonatomic) location_corelocation_state_t *state; @end -@implementation Delegate; +@implementation LocationDelegate; - (void)start { - self.locationManager = [[CLLocationManager alloc] init]; - self.locationManager.delegate = self; + self.locationManager = [[CLLocationManager alloc] init]; + self.locationManager.delegate = self; + self.locationManager.distanceFilter = 50000; + self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer; + + CLAuthorizationStatus authStatus = + [CLLocationManager authorizationStatus]; + + if (authStatus != kCLAuthorizationStatusNotDetermined && + authStatus != kCLAuthorizationStatusAuthorized) { + fputs(_("Not authorized to obtain location" + " from CoreLocation.\n"), stderr); + [self markError]; + } else { + [self.locationManager startUpdatingLocation]; + } +} - CLAuthorizationStatus authStatus = - [CLLocationManager authorizationStatus]; +- (void)markError +{ + [self.state->private->lock lock]; - if (authStatus != kCLAuthorizationStatusNotDetermined && - authStatus != kCLAuthorizationStatusAuthorized) { - fputs(_("Not authorized to obtain location" - " from CoreLocation.\n"), stderr); - CFRunLoopStop(CFRunLoopGetCurrent()); - } + self.state->error = 1; - [self.locationManager startUpdatingLocation]; -} + [self.state->private->lock unlock]; -- (void)stop -{ - [self.locationManager stopUpdatingLocation]; - CFRunLoopStop(CFRunLoopGetCurrent()); + pipeutils_signal(self.state->pipe_fd_write); } - (void)locationManager:(CLLocationManager *)manager - didUpdateLocations:(NSArray *)locations + didUpdateLocations:(NSArray *)locations { - CLLocation *newLocation = [locations firstObject]; - self.latitude = newLocation.coordinate.latitude; - self.longitude = newLocation.coordinate.longitude; - self.success = YES; + CLLocation *newLocation = [locations firstObject]; + + [self.state->private->lock lock]; + + self.state->latitude = newLocation.coordinate.latitude; + self.state->longitude = newLocation.coordinate.longitude; + self.state->available = 1; - [self stop]; + [self.state->private->lock unlock]; + + pipeutils_signal(self.state->pipe_fd_write); } - (void)locationManager:(CLLocationManager *)manager - didFailWithError:(NSError *)error + didFailWithError:(NSError *)error { - fprintf(stderr, _("Error obtaining location from CoreLocation: %s\n"), - [[error localizedDescription] UTF8String]); - [self stop]; + fprintf(stderr, _("Error obtaining location from CoreLocation: %s\n"), + [[error localizedDescription] UTF8String]); + [self markError]; } - (void)locationManager:(CLLocationManager *)manager - didChangeAuthorizationStatus:(CLAuthorizationStatus)status + didChangeAuthorizationStatus:(CLAuthorizationStatus)status +{ + if (status == kCLAuthorizationStatusNotDetermined) { + fputs(_("Waiting for authorization to obtain location...\n"), stderr); + } else if (status != kCLAuthorizationStatusAuthorized) { + fputs(_("Request for location was not authorized!\n"), stderr); + [self markError]; + } +} + +@end + + +// Callback when the pipe is closed. +// +// Stops the run loop causing the thread to end. +static void +pipe_close_callback( + CFFileDescriptorRef fdref, CFOptionFlags callBackTypes, void *info) +{ + CFFileDescriptorInvalidate(fdref); + CFRelease(fdref); + + CFRunLoopStop(CFRunLoopGetCurrent()); +} + + +@interface LocationThread : NSThread +@property (nonatomic) location_corelocation_state_t *state; +@end + +@implementation LocationThread; + +// Run loop for location provider thread. +- (void)main { - if (status == kCLAuthorizationStatusNotDetermined) { - fputs(_("Waiting for authorization to obtain location...\n"), - stderr); - } else if (status != kCLAuthorizationStatusAuthorized) { - fputs(_("Request for location was not authorized!\n"), - stderr); - [self stop]; - } + @autoreleasepool { + LocationDelegate *locationDelegate = [[LocationDelegate alloc] init]; + locationDelegate.state = self.state; + + // Start the location delegate on the run loop in this thread. + [locationDelegate performSelector:@selector(start) + withObject:nil afterDelay:0]; + + // Create a callback that is triggered when the pipe is closed. This will + // trigger the main loop to quit and the thread to stop. + CFFileDescriptorRef fdref = CFFileDescriptorCreate( + kCFAllocatorDefault, self.state->pipe_fd_write, false, + pipe_close_callback, NULL); + CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack); + CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource( + kCFAllocatorDefault, fdref, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); + + // Run the loop + CFRunLoopRun(); + + close(self.state->pipe_fd_write); + } } @end int -location_corelocation_init(void *state) +location_corelocation_init(location_corelocation_state_t *state) { - return 0; + return 0; } int -location_corelocation_start(void *state) +location_corelocation_start(location_corelocation_state_t *state) { - return 0; + state->pipe_fd_read = -1; + state->pipe_fd_write = -1; + + state->available = 0; + state->error = 0; + state->latitude = 0; + state->longitude = 0; + + state->private = malloc(sizeof(location_corelocation_private_t)); + if (state->private == NULL) return -1; + + int pipefds[2]; + int r = pipeutils_create_nonblocking(pipefds); + if (r < 0) { + fputs(_("Failed to start CoreLocation provider!\n"), stderr); + free(state->private); + return -1; + } + + state->pipe_fd_read = pipefds[0]; + state->pipe_fd_write = pipefds[1]; + + pipeutils_signal(state->pipe_fd_write); + + state->private->lock = [[NSLock alloc] init]; + + LocationThread *thread = [[LocationThread alloc] init]; + thread.state = state; + [thread start]; + state->private->thread = thread; + + return 0; } void -location_corelocation_free(void *state) +location_corelocation_free(location_corelocation_state_t *state) { + if (state->pipe_fd_read != -1) { + close(state->pipe_fd_read); + } + + free(state->private); } void location_corelocation_print_help(FILE *f) { - fputs(_("Use the location as discovered by the Corelocation provider.\n"), f); - fputs("\n", f); - - fprintf(f, _("NOTE: currently Redshift doesn't recheck %s once started,\n" - "which means it has to be restarted to take notice after travel.\n"), - "CoreLocation"); - fputs("\n", f); + fputs(_("Use the location as discovered by the Corelocation provider.\n"), f); + fputs("\n", f); } int -location_corelocation_set_option(void *state, - const char *key, const char *value) +location_corelocation_set_option( + location_corelocation_state_t *state, const char *key, const char *value) { - fprintf(stderr, _("Unknown method parameter: `%s'.\n"), key); - return -1; + fprintf(stderr, _("Unknown method parameter: `%s'.\n"), key); + return -1; } int -location_corelocation_get_location(void *state, - location_t *location) +location_corelocation_get_fd(location_corelocation_state_t *state) { - int result = -1; + return state->pipe_fd_read; +} + +int location_corelocation_handle( + location_corelocation_state_t *state, + location_t *location, int *available) +{ + pipeutils_handle_signal(state->pipe_fd_read); + + [state->private->lock lock]; + + int error = state->error; + location->lat = state->latitude; + location->lon = state->longitude; + *available = state->available; - @autoreleasepool { - Delegate *delegate = [[Delegate alloc] init]; - [delegate performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO]; - CFRunLoopRun(); + [state->private->lock unlock]; - if (delegate.success) { - location->lat = delegate.latitude; - location->lon = delegate.longitude; - result = 0; - } - } + if (error) return -1; - return result; + return 0; } diff --git a/src/location-geoclue.c b/src/location-geoclue.c deleted file mode 100644 index e24c2d2..0000000 --- a/src/location-geoclue.c +++ /dev/null @@ -1,218 +0,0 @@ -/* location-geoclue.c -- Geoclue location provider source - 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 <http://www.gnu.org/licenses/>. - - Copyright (c) 2010 Mathieu Trudel-Lapierre <mathieu-tl@ubuntu.com> -*/ - -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -#include <geoclue/geoclue-master.h> -#include <geoclue/geoclue-position.h> - -#include <glib.h> -#include <glib-object.h> - -#include "location-geoclue.h" -#include "redshift.h" - -#ifdef ENABLE_NLS -# include <libintl.h> -# define _(s) gettext(s) -#else -# define _(s) s -#endif - -#define DEFAULT_PROVIDER "org.freedesktop.Geoclue.Providers.UbuntuGeoIP" -#define DEFAULT_PROVIDER_PATH "/org/freedesktop/Geoclue/Providers/UbuntuGeoIP" - -int -location_geoclue_init(location_geoclue_state_t *state) -{ -#if !GLIB_CHECK_VERSION(2, 35, 0) - g_type_init(); -#endif - - state->position = NULL; - state->provider = NULL; - state->provider_path = NULL; - - return 0; -} - -int -location_geoclue_start(location_geoclue_state_t *state) -{ - if (state->provider && state->provider_path) { - state->position = geoclue_position_new(state->provider, - state->provider_path); - } else { - if (getenv("DISPLAY") == NULL || *getenv("DISPLAY") == '\0') { - /* TODO This (hack) should be removed when GeoClue has been patched. */ - putenv("DISPLAY=:0"); - } - GError *error = NULL; - GeoclueMaster *master = geoclue_master_get_default(); - GeoclueMasterClient *client = geoclue_master_create_client(master, - NULL, &error); - g_object_unref(master); - - if (client == NULL) { - if (error != NULL) { - g_printerr(_("Unable to obtain master client: %s\n"), - error->message); - g_error_free(error); - } else { - g_printerr(_("Unable to obtain master client\n")); - } - return -1; - } - - if (!geoclue_master_client_set_requirements(client, - GEOCLUE_ACCURACY_LEVEL_REGION, - 0, FALSE, - GEOCLUE_RESOURCE_NETWORK, - &error)) { - if (error != NULL) { - g_printerr(_("Can't set requirements for master: %s\n"), - error->message); - g_error_free(error); - } else { - g_printerr(_("Can't set requirements for master\n")); - } - g_object_unref(client); - - return -1; - } - - state->position = geoclue_master_client_create_position(client, NULL); - - g_object_unref(client); - } - - gchar *name = NULL; - - if (geoclue_provider_get_provider_info(GEOCLUE_PROVIDER(state->position), - &name, NULL, NULL)) { - fprintf(stdout, _("Started Geoclue provider `%s'.\n"), name); - g_free(name); - } else { - fputs(_("Could not find a usable Geoclue provider.\n"), stderr); - fputs(_("Try setting name and path to specify which to use.\n"), stderr); - return -1; - } - - return 0; -} - -void -location_geoclue_free(location_geoclue_state_t *state) -{ - if (state->position != NULL) g_object_unref(state->position); - free(state->provider); - free(state->provider_path); -} - -void -location_geoclue_print_help(FILE *f) -{ - fputs(_("Use the location as discovered by a Geoclue provider.\n"), f); - fputs("\n", f); - - /* TRANSLATORS: Geoclue help output - left column must not be translated */ - fputs(_(" name=N\tName of Geoclue provider (or `default')\n" - " path=N\tPath of Geoclue provider (or `default')\n"), f); - fputs("\n", f); - fprintf(f, _("NOTE: currently Redshift doesn't recheck %s once started,\n" - "which means it has to be restarted to take notice after travel.\n"), - "GeoClue"); - fputs("\n", f); -} - -int -location_geoclue_set_option(location_geoclue_state_t *state, - const char *key, const char *value) -{ - const char *provider = NULL; - const char *path = NULL; - - /* Parse string value */ - if (strcasecmp(key, "name") == 0) { - if (strcasecmp(value, "default") == 0) { - provider = DEFAULT_PROVIDER; - } else { - provider = value; - } - - state->provider = strdup(provider); - if (state->provider == NULL) { - perror("strdup"); - return -1; - } - } else if (strcasecmp(key, "path") == 0) { - if (value != NULL && strcasecmp(value, "default") == 0) { - path = DEFAULT_PROVIDER_PATH; - } else { - path = value; - } - - state->provider_path = strdup(path); - if (state->provider_path == NULL) { - perror("strdup"); - return -1; - } - } else { - fprintf(stderr, _("Unknown method parameter: `%s'.\n"), key); - return -1; - } - - return 0; -} - -int -location_geoclue_get_location(location_geoclue_state_t *state, - location_t *location) -{ - GeocluePositionFields fields; - GError *error = NULL; - double latitude = 0, longitude = 0; - - fields = geoclue_position_get_position(state->position, NULL, - &latitude, &longitude, NULL, - NULL, &error); - if (error) { - g_printerr(_("Could not get location: %s.\n"), error->message); - g_error_free(error); - return -1; - } - - if (fields & GEOCLUE_POSITION_FIELDS_LATITUDE && - fields & GEOCLUE_POSITION_FIELDS_LONGITUDE) { - fprintf(stdout, _("According to the geoclue provider" - " we're at: %.2f, %.2f\n"), - latitude, longitude); - } else { - g_warning(_("Provider does not have a valid location available.")); - return -1; - } - - location->lat = latitude; - location->lon = longitude; - - return 0; -} diff --git a/src/location-geoclue.h b/src/location-geoclue.h deleted file mode 100644 index 3847ee2..0000000 --- a/src/location-geoclue.h +++ /dev/null @@ -1,46 +0,0 @@ -/* location-geoclue.h -- Geoclue location provider header - 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 <http://www.gnu.org/licenses/>. - - Copyright (c) 2010 Mathieu Trudel-Lapierre <mathieu-tl@ubuntu.com> -*/ - -#ifndef REDSHIFT_LOCATION_GEOCLUE_H -#define REDSHIFT_LOCATION_GEOCLUE_H - -#include <stdio.h> -#include <geoclue/geoclue-position.h> - -#include "redshift.h" - -typedef struct { - GeocluePosition *position; /* main geoclue object */ - char *provider; /* name of a geoclue provider */ - char *provider_path; /* path of the geoclue provider */ -} location_geoclue_state_t; - -int location_geoclue_init(location_geoclue_state_t *state); -int location_geoclue_start(location_geoclue_state_t *state); -void location_geoclue_free(location_geoclue_state_t *state); - -void location_geoclue_print_help(FILE *f); -int location_geoclue_set_option(location_geoclue_state_t *state, - const char *key, const char *value); - -int location_geoclue_get_location(location_geoclue_state_t *state, - location_t *loc); - - -#endif /* ! REDSHIFT_LOCATION_GEOCLUE_H */ diff --git a/src/location-geoclue2.c b/src/location-geoclue2.c index abccbd3..6ebedb4 100644 --- a/src/location-geoclue2.c +++ b/src/location-geoclue2.c @@ -14,7 +14,7 @@ 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) 2014 Jon Lund Steffensen <jonlst@gmail.com> + Copyright (c) 2014-2017 Jon Lund Steffensen <jonlst@gmail.com> */ #include <stdio.h> @@ -26,6 +26,7 @@ #include "location-geoclue2.h" #include "redshift.h" +#include "pipeutils.h" #ifdef ENABLE_NLS # include <libintl.h> @@ -34,62 +35,41 @@ # define _(s) s #endif +#define DBUS_ACCESS_ERROR "org.freedesktop.DBus.Error.AccessDenied" -typedef struct { - GMainLoop *loop; - int available; - location_t location; -} get_location_data_t; - - -int -location_geoclue2_init(void *state) +/* Print the message explaining denial from GeoClue. */ +static void +print_denial_message() { -#if !GLIB_CHECK_VERSION(2, 35, 0) - g_type_init(); -#endif - return 0; + g_printerr(_( + "Access to the current location was denied by GeoClue!\n" + "Make sure that location services are enabled and that" + " Redshift is permitted\nto use location services." + " See https://github.com/jonls/redshift#faq for more\n" + "information.\n")); } -int -location_geoclue2_start(void *state) +/* Indicate an unrecoverable error during GeoClue2 communication. */ +static void +mark_error(location_geoclue2_state_t *state) { - return 0; -} + g_mutex_lock(&state->lock); -void -location_geoclue2_free(void *state) -{ -} + state->error = 1; -void -location_geoclue2_print_help(FILE *f) -{ - fputs(_("Use the location as discovered by a GeoClue2 provider.\n"), f); - fputs("\n", f); + g_mutex_unlock(&state->lock); - fprintf(f, _("NOTE: currently Redshift doesn't recheck %s once started,\n" - "which means it has to be restarted to take notice after travel.\n"), - "GeoClue2"); - fputs("\n", f); -} - -int -location_geoclue2_set_option(void *state, - const char *key, const char *value) -{ - fprintf(stderr, _("Unknown method parameter: `%s'.\n"), key); - return -1; + pipeutils_signal(state->pipe_fd_write); } /* Handle position change callbacks */ static void geoclue_client_signal_cb(GDBusProxy *client, gchar *sender_name, gchar *signal_name, GVariant *parameters, - gpointer *user_data) + gpointer user_data) { - get_location_data_t *data = (get_location_data_t *)user_data; + location_geoclue2_state_t *state = user_data; /* Only handle LocationUpdated signals */ if (g_strcmp0(signal_name, "LocationUpdated") != 0) { @@ -102,34 +82,38 @@ geoclue_client_signal_cb(GDBusProxy *client, gchar *sender_name, /* 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); + GDBusProxy *location = g_dbus_proxy_new_sync( + g_dbus_proxy_get_connection(client), + 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); + mark_error(state); return; } + g_mutex_lock(&state->lock); + /* Read location properties */ - GVariant *lat_v = g_dbus_proxy_get_cached_property(location, - "Latitude"); - data->location.lat = g_variant_get_double(lat_v); + GVariant *lat_v = g_dbus_proxy_get_cached_property( + location, "Latitude"); + state->latitude = g_variant_get_double(lat_v); + + GVariant *lon_v = g_dbus_proxy_get_cached_property( + location, "Longitude"); + state->longitude = g_variant_get_double(lon_v); - GVariant *lon_v = g_dbus_proxy_get_cached_property(location, - "Longitude"); - data->location.lon = g_variant_get_double(lon_v); + state->available = 1; - data->available = 1; + g_mutex_unlock(&state->lock); - /* Return from main loop */ - g_main_loop_quit(data->loop); + pipeutils_signal(state->pipe_fd_write); } /* Callback when GeoClue name appears on the bus */ @@ -137,22 +121,23 @@ static void on_name_appeared(GDBusConnection *conn, const gchar *name, const gchar *name_owner, gpointer user_data) { - get_location_data_t *data = (get_location_data_t *)user_data; + location_geoclue2_state_t *state = user_data; /* Obtain GeoClue Manager */ GError *error = NULL; - GDBusProxy *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); + GDBusProxy *geoclue_manager = g_dbus_proxy_new_sync( + conn, + 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); + mark_error(state); return; } @@ -169,6 +154,7 @@ on_name_appeared(GDBusConnection *conn, const gchar *name, error->message); g_error_free(error); g_object_unref(geoclue_manager); + mark_error(state); return; } @@ -177,20 +163,21 @@ on_name_appeared(GDBusConnection *conn, const gchar *name, /* Obtain GeoClue client */ error = NULL; - GDBusProxy *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); + GDBusProxy *geoclue_client = g_dbus_proxy_new_sync( + conn, + 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); + mark_error(state); return; } @@ -198,15 +185,15 @@ on_name_appeared(GDBusConnection *conn, const gchar *name, /* Set desktop id (basename of the .desktop file) */ 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", - "DesktopId", - g_variant_new("s", "redshift")), - G_DBUS_CALL_FLAGS_NONE, - -1, NULL, &error); + GVariant *ret_v = g_dbus_proxy_call_sync( + geoclue_client, + "org.freedesktop.DBus.Properties.Set", + g_variant_new("(ssv)", + "org.freedesktop.GeoClue2.Client", + "DesktopId", + g_variant_new("s", "redshift")), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); if (ret_v == NULL) { /* Ignore this error for now. The property is not available in early versions of GeoClue2. */ @@ -216,20 +203,22 @@ on_name_appeared(GDBusConnection *conn, const gchar *name, /* Set distance threshold */ error = NULL; - 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", 50000)), - G_DBUS_CALL_FLAGS_NONE, - -1, NULL, &error); + 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", 50000)), + 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); + mark_error(state); return; } @@ -238,7 +227,7 @@ on_name_appeared(GDBusConnection *conn, const gchar *name, /* Attach signal callback to client */ g_signal_connect(geoclue_client, "g-signal", G_CALLBACK(geoclue_client_signal_cb), - data); + user_data); /* Start GeoClue client */ error = NULL; @@ -250,9 +239,18 @@ on_name_appeared(GDBusConnection *conn, const gchar *name, if (ret_v == NULL) { g_printerr(_("Unable to start GeoClue client: %s.\n"), error->message); + if (g_dbus_error_is_remote_error(error)) { + gchar *dbus_error = g_dbus_error_get_remote_error( + error); + if (g_strcmp0(dbus_error, DBUS_ACCESS_ERROR) == 0) { + print_denial_message(); + } + g_free(dbus_error); + } g_error_free(error); g_object_unref(geoclue_client); g_object_unref(geoclue_manager); + mark_error(state); return; } @@ -264,34 +262,159 @@ static void on_name_vanished(GDBusConnection *connection, const gchar *name, gpointer user_data) { - get_location_data_t *data = (get_location_data_t *)user_data; + location_geoclue2_state_t *state = user_data; + + g_mutex_lock(&state->lock); - g_fprintf(stderr, _("Unable to connect to GeoClue.\n")); + state->available = 0; - g_main_loop_quit(data->loop); + g_mutex_unlock(&state->lock); + + pipeutils_signal(state->pipe_fd_write); } -int -location_geoclue2_get_location(void *state, - location_t *location) +/* Callback when the pipe to the main thread is closed. */ +static gboolean +on_pipe_closed(GIOChannel *channel, GIOCondition condition, gpointer user_data) { - get_location_data_t data; - data.available = 0; - - guint watcher_id = g_bus_watch_name(G_BUS_TYPE_SYSTEM, - "org.freedesktop.GeoClue2", - G_BUS_NAME_WATCHER_FLAGS_AUTO_START, - on_name_appeared, - on_name_vanished, - &data, NULL); - data.loop = g_main_loop_new(NULL, FALSE); - g_main_loop_run(data.loop); + location_geoclue2_state_t *state = user_data; + g_main_loop_quit(state->loop); + + return FALSE; +} + + +/* Run loop for location provider thread. */ +void * +run_geoclue2_loop(void *state_) +{ + location_geoclue2_state_t *state = state_; + + GMainContext *context = g_main_context_new(); + g_main_context_push_thread_default(context); + state->loop = g_main_loop_new(context, FALSE); + + guint watcher_id = g_bus_watch_name( + G_BUS_TYPE_SYSTEM, + "org.freedesktop.GeoClue2", + G_BUS_NAME_WATCHER_FLAGS_AUTO_START, + on_name_appeared, + on_name_vanished, + state, NULL); + + /* Listen for closure of pipe */ + GIOChannel *pipe_channel = g_io_channel_unix_new(state->pipe_fd_write); + GSource *pipe_source = g_io_create_watch( + pipe_channel, G_IO_IN | G_IO_HUP | G_IO_ERR); + g_source_set_callback( + pipe_source, (GSourceFunc)on_pipe_closed, state, NULL); + g_source_attach(pipe_source, context); + + g_main_loop_run(state->loop); + + g_source_unref(pipe_source); + g_io_channel_unref(pipe_channel); + close(state->pipe_fd_write); g_bus_unwatch_name(watcher_id); - if (!data.available) return -1; + g_main_loop_unref(state->loop); + g_main_context_unref(context); + + return NULL; +} + +int +location_geoclue2_init(location_geoclue2_state_t *state) +{ +#if !GLIB_CHECK_VERSION(2, 35, 0) + g_type_init(); +#endif + return 0; +} + +int +location_geoclue2_start(location_geoclue2_state_t *state) +{ + state->pipe_fd_read = -1; + state->pipe_fd_write = -1; + + state->available = 0; + state->error = 0; + state->latitude = 0; + state->longitude = 0; + + int pipefds[2]; + int r = pipeutils_create_nonblocking(pipefds); + if (r < 0) { + fputs(_("Failed to start GeoClue2 provider!\n"), stderr); + return -1; + } + + state->pipe_fd_read = pipefds[0]; + state->pipe_fd_write = pipefds[1]; + + pipeutils_signal(state->pipe_fd_write); + + g_mutex_init(&state->lock); + state->thread = g_thread_new("geoclue2", run_geoclue2_loop, state); + + return 0; +} + +void +location_geoclue2_free(location_geoclue2_state_t *state) +{ + if (state->pipe_fd_read != -1) { + close(state->pipe_fd_read); + } + + /* Closing the pipe should cause the thread to exit. */ + g_thread_join(state->thread); + state->thread = NULL; + + g_mutex_clear(&state->lock); +} + +void +location_geoclue2_print_help(FILE *f) +{ + fputs(_("Use the location as discovered by a GeoClue2 provider.\n"), + f); + fputs("\n", f); +} + +int +location_geoclue2_set_option(location_geoclue2_state_t *state, + const char *key, const char *value) +{ + fprintf(stderr, _("Unknown method parameter: `%s'.\n"), key); + return -1; +} + +int +location_geoclue2_get_fd(location_geoclue2_state_t *state) +{ + return state->pipe_fd_read; +} + +int +location_geoclue2_handle( + location_geoclue2_state_t *state, + location_t *location, int *available) +{ + pipeutils_handle_signal(state->pipe_fd_read); + + g_mutex_lock(&state->lock); + + int error = state->error; + location->lat = state->latitude; + location->lon = state->longitude; + *available = state->available; + + g_mutex_unlock(&state->lock); - *location = data.location; + if (error) return -1; return 0; } diff --git a/src/location-geoclue2.h b/src/location-geoclue2.h index c3c377b..2f04eea 100644 --- a/src/location-geoclue2.h +++ b/src/location-geoclue2.h @@ -14,7 +14,7 @@ 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) 2014 Jon Lund Steffensen <jonlst@gmail.com> + Copyright (c) 2014-2017 Jon Lund Steffensen <jonlst@gmail.com> */ #ifndef REDSHIFT_LOCATION_GEOCLUE2_H @@ -22,19 +22,34 @@ #include <stdio.h> +#include <glib.h> + #include "redshift.h" +typedef struct { + GMainLoop *loop; + GThread *thread; + GMutex lock; + int pipe_fd_read; + int pipe_fd_write; + int available; + int error; + float latitude; + float longitude; +} location_geoclue2_state_t; + -int location_geoclue2_init(void *state); -int location_geoclue2_start(void *state); -void location_geoclue2_free(void *state); +int location_geoclue2_init(location_geoclue2_state_t *state); +int location_geoclue2_start(location_geoclue2_state_t *state); +void location_geoclue2_free(location_geoclue2_state_t *state); void location_geoclue2_print_help(FILE *f); -int location_geoclue2_set_option(void *state, +int location_geoclue2_set_option(location_geoclue2_state_t *state, const char *key, const char *value); -int location_geoclue2_get_location(void *state, - location_t *loc); +int location_geoclue2_get_fd(location_geoclue2_state_t *state); +int location_geoclue2_handle(location_geoclue2_state_t *state, + location_t *location, int *available); #endif /* ! REDSHIFT_LOCATION_GEOCLUE2_H */ diff --git a/src/location-manual.c b/src/location-manual.c index c5da074..8ce324c 100644 --- a/src/location-manual.c +++ b/src/location-manual.c @@ -14,7 +14,7 @@ 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) 2010-2014 Jon Lund Steffensen <jonlst@gmail.com> + Copyright (c) 2010-2017 Jon Lund Steffensen <jonlst@gmail.com> */ #include <stdio.h> @@ -101,10 +101,17 @@ location_manual_set_option(location_manual_state_t *state, const char *key, } int -location_manual_get_location(location_manual_state_t *state, - location_t *loc) +location_manual_get_fd(location_manual_state_t *state) { - *loc = state->loc; + return -1; +} + +int +location_manual_handle( + location_manual_state_t *state, location_t *location, int *available) +{ + *location = state->loc; + *available = 1; return 0; } diff --git a/src/location-manual.h b/src/location-manual.h index e70d9cf..7094e9a 100644 --- a/src/location-manual.h +++ b/src/location-manual.h @@ -14,7 +14,7 @@ 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) 2010-2014 Jon Lund Steffensen <jonlst@gmail.com> + Copyright (c) 2010-2017 Jon Lund Steffensen <jonlst@gmail.com> */ #ifndef REDSHIFT_LOCATION_MANUAL_H @@ -38,8 +38,9 @@ void location_manual_print_help(FILE *f); int location_manual_set_option(location_manual_state_t *state, const char *key, const char *value); -int location_manual_get_location(location_manual_state_t *state, - location_t *loc); +int location_manual_get_fd(location_manual_state_t *state); +int location_manual_handle( + location_manual_state_t *state, location_t *location, int *available); #endif /* ! REDSHIFT_LOCATION_MANUAL_H */ diff --git a/src/pipeutils.c b/src/pipeutils.c new file mode 100644 index 0000000..75302cb --- /dev/null +++ b/src/pipeutils.c @@ -0,0 +1,98 @@ +/* pipeutils.c -- Utilities for using pipes as signals + 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 <http://www.gnu.org/licenses/>. + + Copyright (c) 2017 Jon Lund Steffensen <jonlst@gmail.com> +*/ + +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> + + +#ifndef _WIN32 + +/* Create non-blocking set of pipe fds. */ +int +pipeutils_create_nonblocking(int pipefds[2]) +{ + int r = pipe(pipefds); + if (r == -1) { + perror("pipe"); + return -1; + } + + int flags = fcntl(pipefds[0], F_GETFL); + if (flags == -1) { + perror("fcntl"); + close(pipefds[0]); + close(pipefds[1]); + return -1; + } + + r = fcntl(pipefds[0], F_SETFL, flags | O_NONBLOCK); + if (r == -1) { + perror("fcntl"); + close(pipefds[0]); + close(pipefds[1]); + return -1; + } + + flags = fcntl(pipefds[1], F_GETFL); + if (flags == -1) { + perror("fcntl"); + close(pipefds[0]); + close(pipefds[1]); + return -1; + } + + r = fcntl(pipefds[1], F_SETFL, flags | O_NONBLOCK); + if (r == -1) { + perror("fcntl"); + close(pipefds[0]); + close(pipefds[1]); + return -1; + } + + return 0; +} + +#else /* _WIN32 */ + +/* Create non-blocking set of pipe fds. + + Not supported on Windows! Always fails. */ +int +pipeutils_create_nonblocking(int pipefds[2]) +{ + return -1; +} + +#endif + +/* Signal on write-end of pipe. */ +void +pipeutils_signal(int write_fd) +{ + write(write_fd, "", 1); +} + +/* Mark signal as handled on read-end of pipe. */ +void +pipeutils_handle_signal(int read_fd) +{ + char data; + read(read_fd, &data, 1); +} diff --git a/src/pipeutils.h b/src/pipeutils.h new file mode 100644 index 0000000..69c3350 --- /dev/null +++ b/src/pipeutils.h @@ -0,0 +1,28 @@ +/* pipeutils.h -- Utilities for using pipes as signals + 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 <http://www.gnu.org/licenses/>. + + Copyright (c) 2017 Jon Lund Steffensen <jonlst@gmail.com> +*/ + +#ifndef REDSHIFT_PIPEUTILS_H +#define REDSHIFT_PIPEUTILS_H + +int pipeutils_create_nonblocking(int pipefds[2]); + +void pipeutils_signal(int write_fd); +void pipeutils_handle_signal(int read_fd); + +#endif /* ! REDSHIFT_PIPEUTILS_H */ diff --git a/src/redshift-gtk/Makefile.am b/src/redshift-gtk/Makefile.am index c4ab24f..b7303a2 100644 --- a/src/redshift-gtk/Makefile.am +++ b/src/redshift-gtk/Makefile.am @@ -2,8 +2,9 @@ if ENABLE_GUI redshift_gtk_PYTHON = \ __init__.py \ - utils.py \ - statusicon.py + controller.py \ + statusicon.py \ + utils.py nodist_redshift_gtk_PYTHON = \ defs.py redshift_gtkdir = $(pythondir)/redshift_gtk diff --git a/src/redshift-gtk/controller.py b/src/redshift-gtk/controller.py new file mode 100644 index 0000000..24c58ae --- /dev/null +++ b/src/redshift-gtk/controller.py @@ -0,0 +1,227 @@ +# controller.py -- Redshift child process controller +# 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 <http://www.gnu.org/licenses/>. + +# Copyright (c) 2013-2017 Jon Lund Steffensen <jonlst@gmail.com> + +import os +import re +import fcntl +import signal + +import gi +gi.require_version('GLib', '2.0') + +from gi.repository import GLib, GObject + +from . import defs + + +class RedshiftController(GObject.GObject): + """GObject wrapper around the Redshift 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,)), + 'stopped': (GObject.SIGNAL_RUN_FIRST, None, ()), + } + + 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() + for key in ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES'): + env[key] = '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_signal(self, sg): + """Send signal to child process.""" + os.kill(self._process[0], sg) + + def _child_toggle_inhibit(self): + """Sends a request to the child process to toggle state.""" + self._child_signal(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 + try: + GLib.spawn_check_exit_status(status) + except GLib.GError: + self.emit('error-occured', self._errors) + + GLib.spawn_close_pid(self._process[0]) + self.emit('stopped') + + 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 terminate_child(self): + """Send SIGINT to child process.""" + self._child_signal(signal.SIGINT) + + def kill_child(self): + """Send SIGKILL to child process.""" + self._child_signal(signal.SIGKILL) diff --git a/src/redshift-gtk/statusicon.py b/src/redshift-gtk/statusicon.py index 9d9835d..7fdf9af 100644 --- a/src/redshift-gtk/statusicon.py +++ b/src/redshift-gtk/statusicon.py @@ -14,25 +14,23 @@ # 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 import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GLib, GObject +from gi.repository import Gtk, GLib try: gi.require_version('AppIndicator3', '0.1') @@ -40,218 +38,27 @@ try: 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_signal(self, sg): - """Send signal to child process.""" - os.kill(self._process[0], sg) - - def _child_toggle_inhibit(self): - '''Sends a request to the child process to toggle state''' - self._child_signal(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) - except GLib.GError: - self.emit('error-occured', self._errors) - - GLib.spawn_close_pid(self._process[0]) - Gtk.main_quit() - - 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 terminate_child(self): - """Send SIGINT to child process.""" - self._child_signal(signal.SIGINT) - - def kill_child(self): - """Send SIGKILL to child process.""" - self._child_signal(signal.SIGKILL) - - 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 if appindicator: # Create indicator - self.indicator = appindicator.Indicator.new('redshift', - 'redshift-status-on', - appindicator.IndicatorCategory.APPLICATION_STATUS) + self.indicator = appindicator.Indicator.new( + 'redshift', + 'redshift-status-on', + appindicator.IndicatorCategory.APPLICATION_STATUS) self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE) else: # Create status icon @@ -280,16 +87,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')) @@ -302,44 +110,52 @@ 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) # Set info box text self.change_inhibited(self._controller.inhibited) @@ -362,18 +178,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) @@ -382,29 +198,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() @@ -412,20 +229,24 @@ 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 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.''' - - # Update status icon + might have changed. + """ if appindicator: if not self._controller.inhibited: self.indicator.set_icon('redshift-status-on') @@ -439,67 +260,79 @@ class RedshiftStatusIcon(object): # 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 ''' + """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) + self.info_dialog.destroy() self._controller.terminate_child() return False @@ -526,7 +359,7 @@ def run(): try: # Create status icon - s = RedshiftStatusIcon(c) + RedshiftStatusIcon(c) # Run main loop Gtk.main() diff --git a/src/redshift-gtk/utils.py b/src/redshift-gtk/utils.py index 73c95a9..9365356 100644 --- a/src/redshift-gtk/utils.py +++ b/src/redshift-gtk/utils.py @@ -20,8 +20,14 @@ import ctypes import os import sys -from xdg import BaseDirectory as base -from xdg import DesktopEntry as desktop + +try: + from xdg import BaseDirectory + from xdg import DesktopEntry + has_xdg = True +except ImportError: + has_xdg = False + REDSHIFT_DESKTOP = 'redshift-gtk.desktop' @@ -32,12 +38,13 @@ AUTOSTART_KEYS = (('Hidden', ('true', 'false')), def open_autostart_file(): - autostart_dir = base.save_config_path("autostart") + autostart_dir = BaseDirectory.save_config_path("autostart") autostart_file = os.path.join(autostart_dir, REDSHIFT_DESKTOP) if not os.path.exists(autostart_file): - desktop_files = list(base.load_data_paths("applications", - REDSHIFT_DESKTOP)) + desktop_files = list( + BaseDirectory.load_data_paths( + "applications", REDSHIFT_DESKTOP)) if not desktop_files: raise IOError("Installed redshift desktop file not found!") @@ -45,21 +52,33 @@ def open_autostart_file(): desktop_file_path = desktop_files[0] # Read installed file - dfile = desktop.DesktopEntry(desktop_file_path) + dfile = DesktopEntry.DesktopEntry(desktop_file_path) for key, values in AUTOSTART_KEYS: dfile.set(key, values[False]) dfile.write(filename=autostart_file) else: - dfile = desktop.DesktopEntry(autostart_file) + dfile = DesktopEntry.DesktopEntry(autostart_file) return dfile, autostart_file + +def supports_autostart(): + return has_xdg + + def get_autostart(): + if not has_xdg: + return False + dfile, path = open_autostart_file() check_key, check_values = AUTOSTART_KEYS[0] return dfile.get(check_key) == check_values[True] + def set_autostart(active): + if not has_xdg: + return + dfile, path = open_autostart_file() for key, values in AUTOSTART_KEYS: dfile.set(key, values[active]) @@ -67,6 +86,7 @@ def set_autostart(active): def setproctitle(title): + """Set process title.""" title_bytes = title.encode(sys.getdefaultencoding(), 'replace') buf = ctypes.create_string_buffer(title_bytes) if 'linux' in sys.platform: diff --git a/src/redshift.c b/src/redshift.c index 6eefd7d..f46853b 100644 --- a/src/redshift.c +++ b/src/redshift.c @@ -14,7 +14,7 @@ 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) 2009-2015 Jon Lund Steffensen <jonlst@gmail.com> + Copyright (c) 2009-2017 Jon Lund Steffensen <jonlst@gmail.com> */ #ifdef HAVE_CONFIG_H @@ -29,6 +29,21 @@ #include <locale.h> #include <errno.h> +/* poll.h is not available on Windows but there is no Windows location provider + using polling. On Windows, we just define some stubs to make things compile. + */ +#ifndef _WIN32 +# include <poll.h> +#else +#define POLLIN 0 +struct pollfd { + int fd; + short events; + short revents; +}; +int poll(struct pollfd *fds, int nfds, int timeout) { abort(); return -1; } +#endif + #if defined(HAVE_SIGNAL_H) && !defined(__WIN32__) # include <signal.h> #endif @@ -81,10 +96,6 @@ #include "location-manual.h" -#ifdef ENABLE_GEOCLUE -# include "location-geoclue.h" -#endif - #ifdef ENABLE_GEOCLUE2 # include "location-geoclue2.h" #endif @@ -195,28 +206,17 @@ static const gamma_method_t gamma_methods[] = { /* Union of state data for location providers */ typedef union { location_manual_state_t manual; -#ifdef ENABLE_GEOCLUE - location_geoclue_state_t geoclue; +#ifdef ENABLE_GEOCLUE2 + location_geoclue2_state_t geoclue2; +#endif +#ifdef ENABLE_CORELOCATION + location_corelocation_state_t corelocation; #endif } location_state_t; /* Location provider method structs */ static const location_provider_t location_providers[] = { -#ifdef ENABLE_GEOCLUE - { - "geoclue", - (location_provider_init_func *)location_geoclue_init, - (location_provider_start_func *)location_geoclue_start, - (location_provider_free_func *)location_geoclue_free, - (location_provider_print_help_func *) - location_geoclue_print_help, - (location_provider_set_option_func *) - location_geoclue_set_option, - (location_provider_get_location_func *) - location_geoclue_get_location - }, -#endif #ifdef ENABLE_GEOCLUE2 { "geoclue2", @@ -227,8 +227,8 @@ static const location_provider_t location_providers[] = { location_geoclue2_print_help, (location_provider_set_option_func *) location_geoclue2_set_option, - (location_provider_get_location_func *) - location_geoclue2_get_location + (location_provider_get_fd_func *)location_geoclue2_get_fd, + (location_provider_handle_func *)location_geoclue2_handle }, #endif #ifdef ENABLE_CORELOCATION @@ -241,8 +241,8 @@ static const location_provider_t location_providers[] = { location_corelocation_print_help, (location_provider_set_option_func *) location_corelocation_set_option, - (location_provider_get_location_func *) - location_corelocation_get_location + (location_provider_get_fd_func *)location_corelocation_get_fd, + (location_provider_handle_func *)location_corelocation_handle }, #endif { @@ -254,8 +254,8 @@ static const location_provider_t location_providers[] = { location_manual_print_help, (location_provider_set_option_func *) location_manual_set_option, - (location_provider_get_location_func *) - location_manual_get_location + (location_provider_get_fd_func *)location_manual_get_fd, + (location_provider_handle_func *)location_manual_handle }, { NULL } }; @@ -292,6 +292,9 @@ static const location_provider_t location_providers[] = { #define SLEEP_DURATION 5000 #define SLEEP_DURATION_SHORT 100 +/* Length of fade in numbers of short sleep durations. */ +#define FADE_LENGTH 40 + /* Program modes. */ typedef enum { PROGRAM_MODE_CONTINUAL, @@ -301,12 +304,22 @@ typedef enum { PROGRAM_MODE_MANUAL } program_mode_t; +/* Time range. + Fields are offsets from midnight in seconds. */ +typedef struct { + int start; + int end; +} time_range_t; + /* Transition scheme. The solar elevations at which the transition begins/ends, and the association color settings. */ typedef struct { double high; double low; + int use_time; /* When enabled, ignore elevation and use time ranges. */ + time_range_t dawn; + time_range_t dusk; color_setting_t day; color_setting_t night; } transition_scheme_t; @@ -321,10 +334,25 @@ static const char *period_names[] = { }; -/* Determine which period we are currently in. */ +/* Determine which period we are currently in based on time offset. */ static period_t -get_period(const transition_scheme_t *transition, - double elevation) +get_period_from_time(const transition_scheme_t *transition, int time_offset) +{ + if (time_offset < transition->dawn.start || + time_offset >= transition->dusk.end) { + return PERIOD_NIGHT; + } else if (time_offset >= transition->dawn.end && + time_offset < transition->dusk.start) { + return PERIOD_DAYTIME; + } else { + return PERIOD_TRANSITION; + } +} + +/* Determine which period we are currently in based on solar elevation. */ +static period_t +get_period_from_elevation( + const transition_scheme_t *transition, double elevation) { if (elevation < transition->low) { return PERIOD_NIGHT; @@ -335,10 +363,31 @@ get_period(const transition_scheme_t *transition, } } -/* Determine how far through the transition we are. */ +/* Determine how far through the transition we are based on time offset. */ +static double +get_transition_progress_from_time( + const transition_scheme_t *transition, int time_offset) +{ + if (time_offset < transition->dawn.start || + time_offset >= transition->dusk.end) { + return 0.0; + } else if (time_offset < transition->dawn.end) { + return (transition->dawn.start - time_offset) / + (double)(transition->dawn.start - + transition->dawn.end); + } else if (time_offset > transition->dusk.start) { + return (transition->dusk.end - time_offset) / + (double)(transition->dusk.end - + transition->dusk.start); + } else { + return 1.0; + } +} + +/* Determine how far through the transition we are based on elevation. */ static double -get_transition_progress(const transition_scheme_t *transition, - double elevation) +get_transition_progress_from_elevation( + const transition_scheme_t *transition, double elevation) { if (elevation < transition->low) { return 0.0; @@ -350,6 +399,16 @@ get_transition_progress(const transition_scheme_t *transition, } } +/* Return number of seconds since midnight from timestamp. */ +static int +get_seconds_since_midnight(double timestamp) +{ + time_t t = (time_t)timestamp; + struct tm tm; + localtime_r(&t, &tm); + return tm.tm_sec + tm.tm_min * 60 + tm.tm_hour * 3600; +} + /* Print verbose description of the given period. */ static void print_period(period_t period, double transition) @@ -389,27 +448,52 @@ print_location(const location_t *location) fabs(location->lon), location->lon >= 0.f ? east : west); } -/* Interpolate color setting structs based on solar elevation */ +/* Interpolate color setting structs given alpha. */ static void -interpolate_color_settings(const transition_scheme_t *transition, - double elevation, - color_setting_t *result) +interpolate_color_settings( + const color_setting_t *first, + const color_setting_t *second, + double alpha, + color_setting_t *result) +{ + alpha = CLAMP(0.0, alpha, 1.0); + + result->temperature = (1.0-alpha)*first->temperature + + alpha*second->temperature; + result->brightness = (1.0-alpha)*first->brightness + + alpha*second->brightness; + for (int i = 0; i < 3; i++) { + result->gamma[i] = (1.0-alpha)*first->gamma[i] + + alpha*second->gamma[i]; + } +} + +/* Interpolate color setting structs transition scheme. */ +static void +interpolate_transition_scheme( + const transition_scheme_t *transition, + double alpha, + color_setting_t *result) { const color_setting_t *day = &transition->day; const color_setting_t *night = &transition->night; - double alpha = (transition->low - elevation) / - (transition->low - transition->high); alpha = CLAMP(0.0, alpha, 1.0); + interpolate_color_settings(night, day, alpha, result); +} - result->temperature = (1.0-alpha)*night->temperature + - alpha*day->temperature; - result->brightness = (1.0-alpha)*night->brightness + - alpha*day->brightness; - for (int i = 0; i < 3; i++) { - result->gamma[i] = (1.0-alpha)*night->gamma[i] + - alpha*day->gamma[i]; - } +/* Return 1 if color settings have major differences, otherwise 0. + Used to determine if a fade should be applied in continual mode. */ +static int +color_setting_diff_is_major( + const color_setting_t *first, + const color_setting_t *second) +{ + return (abs(first->temperature - second->temperature) > 25 || + fabsf(first->brightness - second->brightness) > 0.1 || + fabsf(first->gamma[0] - second->gamma[0]) > 0.1 || + fabsf(first->gamma[1] - second->gamma[1]) > 0.1 || + fabsf(first->gamma[2] - second->gamma[2]) > 0.1); } @@ -435,14 +519,14 @@ print_help(const char *program_name) no-wrap */ fputs(_(" -h\t\tDisplay this help message\n" " -v\t\tVerbose output\n" - " -V\t\tShow program version\n"), stdout); + " -V\t\tShow program version\n"), stdout); fputs("\n", stdout); /* TRANSLATORS: help output 4 `list' must not be translated no-wrap */ fputs(_(" -b DAY:NIGHT\tScreen brightness to apply (between 0.1 and 1.0)\n" - " -c FILE\tLoad settings from specified configuration file\n" + " -c FILE\tLoad settings from specified configuration file\n" " -g R:G:B\tAdditional gamma correction to apply\n" " -l LAT:LON\tYour current location\n" " -l PROVIDER\tSelect provider for automatic" @@ -455,17 +539,16 @@ print_help(const char *program_name) " -O TEMP\tOne shot manual mode (set color temperature)\n" " -p\t\tPrint mode (only print parameters and exit)\n" " -x\t\tReset mode (remove adjustment from screen)\n" - " -r\t\tDisable temperature transitions\n" + " -r\t\tDisable fading between color temperatures\n" " -t DAY:NIGHT\tColor temperature to set at daytime/night\n"), stdout); fputs("\n", stdout); /* TRANSLATORS: help output 5 */ - printf(_("The neutral temperature is %uK. Using this value will not\n" - "change the color temperature of the display. Setting the\n" - "color temperature to a value higher than this results in\n" - "more blue light, and setting a lower value will result in\n" - "more red light.\n"), + printf(_("The neutral temperature is %uK. Using this value will not change " + "the color\ntemperature of the display. Setting the color temperature " + "to a value higher\nthan this results in more blue light, and setting " + "a lower value will result in\nmore red light.\n"), NEUTRAL_TEMP); fputs("\n", stdout); @@ -726,6 +809,57 @@ parse_brightness_string(const char *str, float *bright_day, float *bright_night) } } +/* Parse transition time string e.g. "04:50". Returns negative on failure, + otherwise the parsed time is returned as seconds since midnight. */ +static int +parse_transition_time(const char *str, const char **end) +{ + const char *min = NULL; + errno = 0; + long hours = strtol(str, (char **)&min, 10); + if (errno != 0 || min == str || min[0] != ':' || + hours < 0 || hours >= 24) { + return -1; + } + + min += 1; + errno = 0; + long minutes = strtol(min, (char **)end, 10); + if (errno != 0 || *end == min || minutes < 0 || minutes >= 60) { + return -1; + } + + return minutes * 60 + hours * 3600; +} + +/* Parse transition range string e.g. "04:50-6:20". Returns negative on + failure, otherwise zero. Parsed start and end times are returned as seconds + since midnight. */ +static int +parse_transition_range(const char *str, time_range_t *range) +{ + const char *next = NULL; + int start_time = parse_transition_time(str, &next); + if (start_time < 0) return -1; + + int end_time; + if (next[0] == '\0') { + end_time = start_time; + } else if (next[0] == '-') { + next += 1; + const char *end = NULL; + end_time = parse_transition_time(next, &end); + if (end_time < 0 || end[0] != '\0') return -1; + } else { + return -1; + } + + range->start = start_time; + range->end = end_time; + + return 0; +} + /* Check whether gamma is within allowed levels. */ static int gamma_is_valid(const float gamma[3]) @@ -736,7 +870,33 @@ gamma_is_valid(const float gamma[3]) gamma[1] > MAX_GAMMA || gamma[2] < MIN_GAMMA || gamma[2] > MAX_GAMMA); +} +/* Check whether location is valid. + Prints error message on stderr and returns 0 if invalid, otherwise + returns 1. */ +static int +location_is_valid(const location_t *location) +{ + /* Latitude */ + if (location->lat < MIN_LAT || location->lat > MAX_LAT) { + /* TRANSLATORS: Append degree symbols if possible. */ + fprintf(stderr, + _("Latitude must be between %.1f and %.1f.\n"), + MIN_LAT, MAX_LAT); + return 0; + } + + /* Longitude */ + if (location->lon < MIN_LON || location->lon > MAX_LON) { + /* TRANSLATORS: Append degree symbols if possible. */ + fprintf(stderr, + _("Longitude must be between" + " %.1f and %.1f.\n"), MIN_LON, MAX_LON); + return 0; + } + + return 1; } static const gamma_method_t * @@ -769,88 +929,171 @@ find_location_provider(const char *name) return provider; } +/* Wait for location to become available from provider. + Waits until timeout (milliseconds) has elapsed or forever if timeout + is -1. Writes location to loc. Returns -1 on error, + 0 if timeout was reached, 1 if location became available. */ +static int +provider_get_location( + const location_provider_t *provider, location_state_t *state, + int timeout, location_t *loc) +{ + int available = 0; + struct pollfd pollfds[1]; + while (!available) { + int loc_fd = provider->get_fd(state); + if (loc_fd >= 0) { + /* Provider is dynamic. */ + /* TODO: This should use a monotonic time source. */ + double now; + int r = systemtime_get_time(&now); + if (r < 0) { + fputs(_("Unable to read system time.\n"), + stderr); + return -1; + } + + /* Poll on file descriptor until ready. */ + pollfds[0].fd = loc_fd; + pollfds[0].events = POLLIN; + r = poll(pollfds, 1, timeout); + if (r < 0) { + perror("poll"); + return -1; + } else if (r == 0) { + return 0; + } + + double later; + r = systemtime_get_time(&later); + if (r < 0) { + fputs(_("Unable to read system time.\n"), + stderr); + return -1; + } + + /* Adjust timeout by elapsed time */ + if (timeout >= 0) { + timeout -= (later - now) * 1000; + timeout = timeout < 0 ? 0 : timeout; + } + } + + + int r = provider->handle(state, loc, &available); + if (r < 0) return -1; + } + + return 1; +} + +/* Easing function for fade. + See https://github.com/mietek/ease-tween */ +static double +ease_fade(double t) +{ + if (t <= 0) return 0; + if (t >= 1) return 1; + return 1.0042954579734844 * exp( + -6.4041738958415664 * exp(-7.2908241330981340 * t)); +} + /* Run continual mode loop This is the main loop of the continual mode which keeps track of the current time and continuously updates the screen to the appropriate color temperature. */ static int -run_continual_mode(const location_t *loc, +run_continual_mode(const location_provider_t *provider, + location_state_t *location_state, const transition_scheme_t *scheme, const gamma_method_t *method, gamma_state_t *state, - int transition, int verbose) + int use_fade, int verbose) { int r; - /* Make an initial transition from 6500K */ - int short_trans_delta = -1; - int short_trans_len = 10; - - /* Amount of adjustment to apply. At zero the color - temperature will be exactly as calculated, and at one it - will be exactly 6500K. */ - double adjustment_alpha = 1.0; + /* Short fade parameters */ + int fade_length = 0; + int fade_time = 0; + color_setting_t fade_start_interp; r = signals_install_handlers(); if (r < 0) { return r; } - if (verbose) { - printf(_("Status: %s\n"), _("Enabled")); + /* Save previous parameters so we can avoid printing status updates if + the values did not change. */ + period_t prev_period = PERIOD_NONE; + + /* Previous target color setting and current actual color setting. + Actual color setting takes into account the current color fade. */ + color_setting_t prev_target_interp = + { NEUTRAL_TEMP, { 1.0, 1.0, 1.0 }, 1.0 }; + color_setting_t interp = + { NEUTRAL_TEMP, { 1.0, 1.0, 1.0 }, 1.0 }; + + location_t loc = { NAN, NAN }; + int need_location = !scheme->use_time; + if (need_location) { + fputs(_("Waiting for initial location" + " to become available...\n"), stderr); + + /* Get initial location from provider */ + r = provider_get_location(provider, location_state, -1, &loc); + if (r < 0) { + fputs(_("Unable to get location" + " from provider.\n"), stderr); + return -1; + } + + if (!location_is_valid(&loc)) { + fputs(_("Invalid location returned from provider.\n"), + stderr); + return -1; + } + + print_location(&loc); } - /* Save previous colors so we can avoid - printing status updates if the values - did not change. */ - period_t prev_period = PERIOD_NONE; - color_setting_t prev_interp = - { -1, { NAN, NAN, NAN }, NAN }; + if (verbose) { + printf(_("Color temperature: %uK\n"), interp.temperature); + printf(_("Brightness: %.2f\n"), interp.brightness); + } /* Continuously adjust color temperature */ int done = 0; + int prev_disabled = 1; int disabled = 0; + int location_available = 1; while (1) { /* Check to see if disable signal was caught */ - if (disable) { - short_trans_len = 2; - if (!disabled) { - /* Transition to disabled state */ - short_trans_delta = 1; - } else { - /* Transition back to enabled */ - short_trans_delta = -1; - } + if (disable && !done) { disabled = !disabled; disable = 0; - - if (verbose) { - printf(_("Status: %s\n"), disabled ? - _("Disabled") : _("Enabled")); - } } /* Check to see if exit signal was caught */ if (exiting) { if (done) { - /* On second signal stop the ongoing - transition */ - short_trans_delta = 0; - adjustment_alpha = 0.0; + /* On second signal stop the ongoing fade. */ + break; } else { - if (!disabled) { - /* Make a short transition - back to 6500K */ - short_trans_delta = 1; - short_trans_len = 2; - } - done = 1; + disabled = 1; } exiting = 0; } + /* Print status change */ + if (verbose && disabled != prev_disabled) { + printf(_("Status: %s\n"), disabled ? + _("Disabled") : _("Enabled")); + } + + prev_disabled = disabled; + /* Read timestamp */ double now; r = systemtime_get_time(&now); @@ -859,36 +1102,51 @@ run_continual_mode(const location_t *loc, return -1; } - /* Skip over transition if transitions are disabled */ - int set_adjustments = 0; - if (!transition) { - if (short_trans_delta) { - adjustment_alpha = short_trans_delta < 0 ? - 0.0 : 1.0; - short_trans_delta = 0; - set_adjustments = 1; - } + period_t period; + double transition_prog; + if (scheme->use_time) { + int time_offset = get_seconds_since_midnight(now); + + period = get_period_from_time(scheme, time_offset); + transition_prog = get_transition_progress_from_time( + scheme, time_offset); + } else { + /* Current angular elevation of the sun */ + double elevation = solar_elevation( + now, loc.lat, loc.lon); + + period = get_period_from_elevation(scheme, elevation); + transition_prog = + get_transition_progress_from_elevation( + scheme, elevation); } - /* Current angular elevation of the sun */ - double elevation = solar_elevation(now, loc->lat, - loc->lon); + /* Use transition progress to get target color + temperature. */ + color_setting_t target_interp; + interpolate_transition_scheme( + scheme, transition_prog, &target_interp); + + if (disabled) { + /* Reset to neutral */ + target_interp.temperature = NEUTRAL_TEMP; + target_interp.brightness = 1.0; + target_interp.gamma[0] = 1.0; + target_interp.gamma[1] = 1.0; + target_interp.gamma[2] = 1.0; + } - /* Use elevation of sun to set color temperature */ - color_setting_t interp; - interpolate_color_settings(scheme, elevation, &interp); + if (done) { + period = PERIOD_NONE; + } /* Print period if it changed during this update, - or if we are in transition. In transition we + or if we are in the transition period. In transition we print the progress, so we always print it in that case. */ - period_t period = get_period(scheme, elevation); if (verbose && (period != prev_period || period == PERIOD_TRANSITION)) { - double transition = - get_transition_progress(scheme, - elevation); - print_period(period, transition); + print_period(period, transition_prog); } /* Activate hooks if period changed */ @@ -896,66 +1154,135 @@ run_continual_mode(const location_t *loc, hooks_signal_period_change(prev_period, period); } - /* Ongoing short transition */ - if (short_trans_delta) { - /* Calculate alpha */ - adjustment_alpha += short_trans_delta * 0.1 / - (float)short_trans_len; - - /* Stop transition when done */ - if (adjustment_alpha <= 0.0 || - adjustment_alpha >= 1.0) { - short_trans_delta = 0; + /* Start fade if the parameter differences are too big to apply + instantly. */ + if (use_fade) { + if ((fade_length == 0 && + color_setting_diff_is_major( + &interp, + &target_interp)) || + (fade_length != 0 && + color_setting_diff_is_major( + &target_interp, + &prev_target_interp))) { + fade_length = FADE_LENGTH; + fade_time = 0; + fade_start_interp = interp; } - - /* Clamp alpha value */ - adjustment_alpha = CLAMP(0.0, adjustment_alpha, 1.0); } - /* Interpolate between 6500K and calculated - temperature */ - interp.temperature = adjustment_alpha*6500 + - (1.0-adjustment_alpha)*interp.temperature; + /* Handle ongoing fade */ + if (fade_length != 0) { + fade_time += 1; + double frac = fade_time / (double)fade_length; + double alpha = CLAMP(0.0, ease_fade(frac), 1.0); + + interpolate_color_settings( + &fade_start_interp, &target_interp, alpha, + &interp); - interp.brightness = adjustment_alpha*1.0 + - (1.0-adjustment_alpha)*interp.brightness; + if (fade_time > fade_length) { + fade_time = 0; + fade_length = 0; + } + } else { + interp = target_interp; + } - /* Quit loop when done */ - if (done && !short_trans_delta) break; + /* Break loop when done and final fade is over */ + if (done && fade_length == 0) break; if (verbose) { - if (interp.temperature != - prev_interp.temperature) { + if (prev_target_interp.temperature != + target_interp.temperature) { printf(_("Color temperature: %uK\n"), - interp.temperature); + target_interp.temperature); } - if (interp.brightness != - prev_interp.brightness) { + if (prev_target_interp.brightness != + target_interp.brightness) { printf(_("Brightness: %.2f\n"), - interp.brightness); + target_interp.brightness); } } /* Adjust temperature */ - if (!disabled || short_trans_delta || set_adjustments) { - r = method->set_temperature(state, &interp); + r = method->set_temperature(state, &interp); + if (r < 0) { + fputs(_("Temperature adjustment failed.\n"), + stderr); + return -1; + } + + /* Save period and target color setting as previous */ + prev_period = period; + prev_target_interp = target_interp; + + /* Sleep length depends on whether a fade is ongoing. */ + int delay = SLEEP_DURATION; + if (fade_length != 0) { + delay = SLEEP_DURATION_SHORT; + } + + /* Update location. */ + int loc_fd = -1; + if (need_location) { + loc_fd = provider->get_fd(location_state); + } + + if (loc_fd >= 0) { + /* Provider is dynamic. */ + struct pollfd pollfds[1]; + pollfds[0].fd = loc_fd; + pollfds[0].events = POLLIN; + int r = poll(pollfds, 1, delay); if (r < 0) { - fputs(_("Temperature adjustment" - " failed.\n"), stderr); + if (errno == EINTR) continue; + perror("poll"); + fputs(_("Unable to get location" + " from provider.\n"), stderr); return -1; + } else if (r == 0) { + continue; } - } - /* Save temperature as previous */ - prev_period = period; - memcpy(&prev_interp, &interp, - sizeof(color_setting_t)); + /* Get new location and availability + information. */ + location_t new_loc; + int new_available; + r = provider->handle( + location_state, &new_loc, + &new_available); + if (r < 0) { + fputs(_("Unable to get location" + " from provider.\n"), stderr); + return -1; + } + + if (!new_available && + new_available != location_available) { + fputs(_("Location is temporarily" + " unavailable; Using previous" + " location until it becomes" + " available...\n"), stderr); + } - /* Sleep for 5 seconds or 0.1 second. */ - if (short_trans_delta) { - systemtime_msleep(SLEEP_DURATION_SHORT); + if (new_available && + (new_loc.lat != loc.lat || + new_loc.lon != loc.lon || + new_available != location_available)) { + loc = new_loc; + print_location(&loc); + } + + location_available = new_available; + + if (!location_is_valid(&loc)) { + fputs(_("Invalid location returned" + " from provider.\n"), stderr); + return -1; + } } else { - systemtime_msleep(SLEEP_DURATION); + systemtime_msleep(delay); } } @@ -965,6 +1292,7 @@ run_continual_mode(const location_t *loc, return 0; } + int main(int argc, char *argv[]) { @@ -983,11 +1311,17 @@ main(int argc, char *argv[]) /* Initialize settings to NULL values. */ char *config_filepath = NULL; - /* Settings for day, night and transition. + /* Settings for day, night and transition period. Initialized to indicate that the values are not set yet. */ transition_scheme_t scheme = { TRANSITION_HIGH, TRANSITION_LOW }; + scheme.use_time = 0; + scheme.dawn.start = -1; + scheme.dawn.end = -1; + scheme.dusk.start = -1; + scheme.dusk.end = -1; + scheme.day.temperature = -1; scheme.day.gamma[0] = NAN; scheme.day.brightness = NAN; @@ -1005,7 +1339,7 @@ main(int argc, char *argv[]) const location_provider_t *provider = NULL; char *provider_args = NULL; - int transition = -1; + int use_fade = -1; program_mode_t mode = PROGRAM_MODE_CONTINUAL; int verbose = 0; char *s; @@ -1135,7 +1469,7 @@ main(int argc, char *argv[]) mode = PROGRAM_MODE_PRINT; break; case 'r': - transition = 0; + use_fade = 0; break; case 't': s = strchr(optarg, ':'); @@ -1194,10 +1528,13 @@ main(int argc, char *argv[]) scheme.night.temperature = atoi(setting->value); } - } else if (strcasecmp(setting->name, - "transition") == 0) { - if (transition < 0) { - transition = !!atoi(setting->value); + } else if (strcasecmp( + setting->name, "transition") == 0 || + strcasecmp(setting->name, "fade") == 0) { + /* "fade" is preferred, "transition" is + deprecated as the setting key. */ + if (use_fade < 0) { + use_fade = !!atoi(setting->value); } } else if (strcasecmp(setting->name, "brightness") == 0) { @@ -1290,6 +1627,34 @@ main(int argc, char *argv[]) exit(EXIT_FAILURE); } } + } else if (strcasecmp(setting->name, + "dawn-time") == 0) { + if (scheme.dawn.start < 0) { + int r = parse_transition_range( + setting->value, &scheme.dawn); + if (r < 0) { + fprintf(stderr, _("Malformed" + " dawn-time" + " setting" + " `%s'.\n"), + setting->value); + exit(EXIT_FAILURE); + } + } + } else if (strcasecmp(setting->name, + "dusk-time") == 0) { + if (scheme.dusk.start < 0) { + int r = parse_transition_range( + setting->value, &scheme.dusk); + if (r < 0) { + fprintf(stderr, _("Malformed" + " dusk-time" + " setting" + " `%s'.\n"), + setting->value); + exit(EXIT_FAILURE); + } + } } else { fprintf(stderr, _("Unknown configuration" " setting `%s'.\n"), @@ -1326,17 +1691,38 @@ main(int argc, char *argv[]) scheme.night.gamma[2] = DEFAULT_GAMMA; } - if (transition < 0) transition = 1; + if (use_fade < 0) use_fade = 1; - location_t loc = { NAN, NAN }; + if (scheme.dawn.start >= 0 || scheme.dawn.end >= 0 || + scheme.dusk.start >= 0 || scheme.dusk.end >= 0) { + if (scheme.dawn.start < 0 || scheme.dawn.end < 0 || + scheme.dusk.start < 0 || scheme.dusk.end < 0) { + fputs(_("Partitial time-configuration not" + " supported!\n"), stderr); + exit(EXIT_FAILURE); + } - /* Initialize location provider. If provider is NULL + if (scheme.dawn.start > scheme.dawn.end || + scheme.dawn.end > scheme.dusk.start || + scheme.dusk.start > scheme.dusk.end) { + fputs(_("Invalid dawn/dusk time configuration!\n"), + stderr); + exit(EXIT_FAILURE); + } + + scheme.use_time = 1; + } + + /* Initialize location provider if needed. If provider is NULL try all providers until one that works is found. */ location_state_t location_state; /* Location is not needed for reset mode and manual mode. */ - if (mode != PROGRAM_MODE_RESET && - mode != PROGRAM_MODE_MANUAL) { + int need_location = + mode != PROGRAM_MODE_RESET && + mode != PROGRAM_MODE_MANUAL && + !scheme.use_time; + if (need_location) { if (provider != NULL) { /* Use provider specified on command line. */ r = provider_try_start(provider, &location_state, @@ -1374,44 +1760,27 @@ main(int argc, char *argv[]) } } - /* Get current location. */ - r = provider->get_location(&location_state, &loc); - if (r < 0) { - fputs(_("Unable to get location from provider.\n"), - stderr); - exit(EXIT_FAILURE); + /* Solar elevations */ + if (scheme.high < scheme.low) { + fprintf(stderr, + _("High transition elevation cannot be lower than" + " the low transition elevation.\n")); + exit(EXIT_FAILURE); } - provider->free(&location_state); - if (verbose) { - print_location(&loc); - - printf(_("Temperatures: %dK at day, %dK at night\n"), - scheme.day.temperature, - scheme.night.temperature); - - /* TRANSLATORS: Append degree symbols if possible. */ + /* TRANSLATORS: Append degree symbols if possible. */ printf(_("Solar elevations: day above %.1f, night below %.1f\n"), scheme.high, scheme.low); } + } - /* Latitude */ - if (loc.lat < MIN_LAT || loc.lat > MAX_LAT) { - /* TRANSLATORS: Append degree symbols if possible. */ - fprintf(stderr, - _("Latitude must be between %.1f and %.1f.\n"), - MIN_LAT, MAX_LAT); - exit(EXIT_FAILURE); - } - - /* Longitude */ - if (loc.lon < MIN_LON || loc.lon > MAX_LON) { - /* TRANSLATORS: Append degree symbols if possible. */ - fprintf(stderr, - _("Longitude must be between" - " %.1f and %.1f.\n"), MIN_LON, MAX_LON); - exit(EXIT_FAILURE); + if (mode != PROGRAM_MODE_RESET && + mode != PROGRAM_MODE_MANUAL) { + if (verbose) { + printf(_("Temperatures: %dK at day, %dK at night\n"), + scheme.day.temperature, + scheme.night.temperature); } /* Color temperature */ @@ -1424,14 +1793,6 @@ main(int argc, char *argv[]) MIN_TEMP, MAX_TEMP); exit(EXIT_FAILURE); } - - /* Solar elevations */ - if (scheme.high < scheme.low) { - fprintf(stderr, - _("High transition elevation cannot be lower than" - " the low transition elevation.\n")); - exit(EXIT_FAILURE); - } } if (mode == PROGRAM_MODE_MANUAL) { @@ -1523,7 +1884,27 @@ main(int argc, char *argv[]) case PROGRAM_MODE_ONE_SHOT: case PROGRAM_MODE_PRINT: { - /* Current angular elevation of the sun */ + location_t loc = { NAN, NAN }; + if (need_location) { + fputs(_("Waiting for current location" + " to become available...\n"), stderr); + + /* Wait for location provider. */ + int r = provider_get_location( + provider, &location_state, -1, &loc); + if (r < 0) { + fputs(_("Unable to get location" + " from provider.\n"), stderr); + exit(EXIT_FAILURE); + } + + if (!location_is_valid(&loc)) { + exit(EXIT_FAILURE); + } + + print_location(&loc); + } + double now; r = systemtime_get_time(&now); if (r < 0) { @@ -1532,48 +1913,60 @@ main(int argc, char *argv[]) exit(EXIT_FAILURE); } - double elevation = solar_elevation(now, loc.lat, loc.lon); + period_t period; + double transition_prog; + if (scheme.use_time) { + int time_offset = get_seconds_since_midnight(now); + period = get_period_from_time(&scheme, time_offset); + transition_prog = get_transition_progress_from_time( + &scheme, time_offset); + } else { + /* Current angular elevation of the sun */ + double elevation = solar_elevation( + now, loc.lat, loc.lon); + if (verbose) { + /* TRANSLATORS: Append degree symbol if + possible. */ + printf(_("Solar elevation: %f\n"), elevation); + } - if (verbose) { - /* TRANSLATORS: Append degree symbol if possible. */ - printf(_("Solar elevation: %f\n"), elevation); + period = get_period_from_elevation(&scheme, elevation); + transition_prog = + get_transition_progress_from_elevation( + &scheme, elevation); } - /* Use elevation of sun to set color temperature */ + /* Use transition progress to set color temperature */ color_setting_t interp; - interpolate_color_settings(&scheme, elevation, &interp); + interpolate_transition_scheme( + &scheme, transition_prog, &interp); if (verbose || mode == PROGRAM_MODE_PRINT) { - period_t period = get_period(&scheme, - elevation); - double transition = - get_transition_progress(&scheme, - elevation); - print_period(period, transition); + print_period(period, transition_prog); printf(_("Color temperature: %uK\n"), interp.temperature); printf(_("Brightness: %.2f\n"), interp.brightness); } - if (mode == PROGRAM_MODE_PRINT) { - exit(EXIT_SUCCESS); - } - - /* Adjust temperature */ - r = method->set_temperature(&state, &interp); - if (r < 0) { - fputs(_("Temperature adjustment failed.\n"), stderr); - method->free(&state); - exit(EXIT_FAILURE); - } + if (mode != PROGRAM_MODE_PRINT) { + /* Adjust temperature */ + r = method->set_temperature(&state, &interp); + if (r < 0) { + fputs(_("Temperature adjustment failed.\n"), + stderr); + method->free(&state); + exit(EXIT_FAILURE); + } - /* In Quartz (OSX) the gamma adjustments will automatically - revert when the process exits. Therefore, we have to loop - until CTRL-C is received. */ - if (strcmp(method->name, "quartz") == 0) { - fputs(_("Press ctrl-c to stop...\n"), stderr); - pause(); + /* In Quartz (macOS) the gamma adjustments will + automatically revert when the process exits. + Therefore, we have to loop until CTRL-C is received. + */ + if (strcmp(method->name, "quartz") == 0) { + fputs(_("Press ctrl-c to stop...\n"), stderr); + pause(); + } } } break; @@ -1582,8 +1975,7 @@ main(int argc, char *argv[]) if (verbose) printf(_("Color temperature: %uK\n"), temp_set); /* Adjust temperature */ - color_setting_t manual; - memcpy(&manual, &scheme.day, sizeof(color_setting_t)); + color_setting_t manual = scheme.day; manual.temperature = temp_set; r = method->set_temperature(&state, &manual); if (r < 0) { @@ -1623,16 +2015,23 @@ main(int argc, char *argv[]) break; case PROGRAM_MODE_CONTINUAL: { - r = run_continual_mode(&loc, &scheme, + r = run_continual_mode(provider, &location_state, &scheme, method, &state, - transition, verbose); + use_fade, verbose); if (r < 0) exit(EXIT_FAILURE); } break; } /* Clean up gamma adjustment state */ - method->free(&state); + if (mode != PROGRAM_MODE_PRINT) { + method->free(&state); + } + + /* Clean up location provider state */ + if (need_location) { + provider->free(&location_state); + } return EXIT_SUCCESS; } diff --git a/src/redshift.h b/src/redshift.h index bac8e34..c659502 100644 --- a/src/redshift.h +++ b/src/redshift.h @@ -89,7 +89,9 @@ typedef void location_provider_free_func(void *state); typedef void location_provider_print_help_func(FILE *f); typedef int location_provider_set_option_func(void *state, const char *key, const char *value); -typedef int location_provider_get_location_func(void *state, location_t *loc); +typedef int location_provider_get_fd_func(void *state); +typedef int location_provider_handle_func( + void *state, location_t *location, int *available); typedef struct { char *name; @@ -106,8 +108,9 @@ typedef struct { /* Set an option key, value-pair. */ location_provider_set_option_func *set_option; - /* Get current location. */ - location_provider_get_location_func *get_location; + /* Listen and handle location updates. */ + location_provider_get_fd_func *get_fd; + location_provider_handle_func *handle; } location_provider_t; diff --git a/src/windows/appicon.rc b/src/windows/appicon.rc new file mode 100644 index 0000000..9980b7e --- /dev/null +++ b/src/windows/appicon.rc @@ -0,0 +1 @@ +AppIcon ICON redshift.ico diff --git a/src/windows/redshift.ico b/src/windows/redshift.ico Binary files differnew file mode 100644 index 0000000..751e6fa --- /dev/null +++ b/src/windows/redshift.ico diff --git a/src/windows/versioninfo.rc b/src/windows/versioninfo.rc new file mode 100644 index 0000000..9ede49d --- /dev/null +++ b/src/windows/versioninfo.rc @@ -0,0 +1,20 @@ +#include "config.h" + +1 VERSIONINFO +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "Redshift Open Source Project" + VALUE "FileDescription", "Redshift" + VALUE "OriginalFilename", "redshift.exe" + VALUE "ProductName", "Redshift" + VALUE "ProductVersion", PACKAGE_VERSION + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END |