diff options
Diffstat (limited to 'big-brother.c')
-rw-r--r-- | big-brother.c | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/big-brother.c b/big-brother.c new file mode 100644 index 0000000..bb489b9 --- /dev/null +++ b/big-brother.c @@ -0,0 +1,404 @@ +/* + * big-brother.c: activity tracker + * + * Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define LIBERTY_WANT_POLLER + +#include "config.h" +#undef PROGRAM_NAME +#define PROGRAM_NAME "big-brother" +#include "liberty/liberty.c" + +#include <locale.h> + +#include <X11/Xlib.h> +#include <X11/Xatom.h> +#include <X11/Xutil.h> +#include <X11/keysym.h> +#include <X11/extensions/sync.h> + +// --- Utilities --------------------------------------------------------------- + +static void +log_message_custom (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + (void) user_data; + FILE *stream = stdout; + + fprintf (stream, PROGRAM_NAME ": "); + fputs (quote, stream); + vfprintf (stream, fmt, ap); + fputs ("\n", stream); +} + +// --- Configuration ----------------------------------------------------------- + +static struct simple_config_item g_config_table[] = +{ + { "idle_timeout", "600", "Timeout for user inactivity (s)" }, + { NULL, NULL, NULL } +}; + +// --- Application ------------------------------------------------------------- + +struct app_context +{ + struct str_map config; ///< Program configuration + struct poller poller; ///< Poller + bool running; ///< Event loop is running + + Display *dpy; ///< X display handle + struct poller_fd x_event; ///< X11 event + + Atom net_active_window; ///< _NET_ACTIVE_WINDOW + Atom net_wm_name; ///< _NET_WM_NAME + + // Window title tracking + + char *current_title; ///< Current window title or NULL + Window current_window; ///< Current window + + // XSync activity tracking + + int xsync_base_event_code; ///< XSync base event code + XSyncCounter idle_counter; ///< XSync IDLETIME counter + XSyncValue idle_timeout; ///< Idle timeout + + XSyncAlarm idle_alarm_inactive; ///< User is inactive + XSyncAlarm idle_alarm_active; ///< User is active +}; + +static void +app_context_init (struct app_context *self) +{ + memset (self, 0, sizeof *self); + + str_map_init (&self->config); + self->config.free = free; + simple_config_load_defaults (&self->config, g_config_table); + + if (!(self->dpy = XOpenDisplay (NULL))) + exit_fatal ("cannot open display"); + + poller_init (&self->poller); + poller_fd_init (&self->x_event, &self->poller, + ConnectionNumber (self->dpy)); + + self->net_active_window = + XInternAtom (self->dpy, "_NET_ACTIVE_WINDOW", true); + self->net_wm_name = + XInternAtom (self->dpy, "_NET_WM_NAME", true); + + // TODO: it is possible to employ a fallback mechanism via XScreenSaver + // by polling the XScreenSaverInfo::idle field, see + // https://www.x.org/releases/X11R7.5/doc/man/man3/Xss.3.html + + int n; + if (!XSyncQueryExtension (self->dpy, &self->xsync_base_event_code, &n) + || !XSyncInitialize (self->dpy, &n, &n)) + exit_fatal ("cannot initialize XSync"); + + // The idle counter is not guaranteed to exist, only SERVERTIME is + XSyncSystemCounter *counters = XSyncListSystemCounters (self->dpy, &n); + while (n--) + { + if (!strcmp (counters[n].name, "IDLETIME")) + self->idle_counter = counters[n].counter; + } + if (!self->idle_counter) + exit_fatal ("idle counter is missing"); + XSyncFreeSystemCounterList (counters); +} + +static void +app_context_free (struct app_context *self) +{ + str_map_free (&self->config); + free (self->current_title); + poller_fd_reset (&self->x_event); + XCloseDisplay (self->dpy); + poller_free (&self->poller); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static char * +x_text_property_to_utf8 (struct app_context *ctx, XTextProperty *prop) +{ +#if ARE_WE_UTF8_YET + Atom utf8_string = XInternAtom (ctx->dpy, "UTF8_STRING", true); + if (prop->encoding == utf8_string) + return xstrdup ((char *) prop->value); +#endif + + int n = 0; + char **list = NULL; + if (XmbTextPropertyToTextList (ctx->dpy, prop, &list, &n) >= Success + && n > 0 && *list) + { + // TODO: convert from locale encoding into UTF-8 + char *result = xstrdup (*list); + XFreeStringList (list); + return result; + } + return NULL; +} + +static char * +x_text_property (struct app_context *ctx, Window window, Atom atom) +{ + XTextProperty name; + XGetTextProperty (ctx->dpy, window, &name, atom); + if (!name.value) + return NULL; + + char *result = x_text_property_to_utf8 (ctx, &name); + XFree (name.value); + return result; +} + +static char * +x_window_title (struct app_context *ctx, Window window) +{ + char *title; + if (!(title = x_text_property (ctx, window, ctx->net_wm_name)) + && !(title = x_text_property (ctx, window, XA_WM_NAME))) + title = xstrdup ("broken"); + return title; +} + +static bool +update_window_title (struct app_context *ctx, char *new_title) +{ + bool changed = !ctx->current_title != !new_title + || (new_title && strcmp (ctx->current_title, new_title)); + free (ctx->current_title); + ctx->current_title = new_title; + return changed; +} + +static void +update_current_window (struct app_context *ctx) +{ + Window root = DefaultRootWindow (ctx->dpy); + + Atom dummy_type; int dummy_format; + unsigned long nitems, dummy_bytes; + unsigned char *p = NULL; + if (XGetWindowProperty (ctx->dpy, root, ctx->net_active_window, + 0L, 1L, false, XA_WINDOW, &dummy_type, &dummy_format, + &nitems, &dummy_bytes, &p) != Success) + return; + + char *new_title = NULL; + if (nitems) + { + Window active_window = *(Window *) p; + XFree (p); + + if (ctx->current_window != active_window && ctx->current_window) + XSelectInput (ctx->dpy, ctx->current_window, 0); + + XSelectInput (ctx->dpy, active_window, PropertyChangeMask); + new_title = x_window_title (ctx, active_window); + ctx->current_window = active_window; + } + if (update_window_title (ctx, new_title)) + print_status ("Window changed: %s", + ctx->current_title ? ctx->current_title : "(none)"); +} + +static void +on_x_property_notify (struct app_context *ctx, XPropertyEvent *ev) +{ + // This is from the EWMH specification, set by the window manager + if (ev->atom == ctx->net_active_window) + update_current_window (ctx); + else if (ev->window == ctx->current_window && ev->atom == ctx->net_wm_name) + { + if (update_window_title (ctx, x_window_title (ctx, ev->window))) + print_status ("Title changed: %s", ctx->current_title); + } +} + +static void +set_idle_alarm (struct app_context *ctx, + XSyncAlarm *alarm, XSyncTestType test, XSyncValue value) +{ + XSyncAlarmAttributes attr; + attr.trigger.counter = ctx->idle_counter; + attr.trigger.test_type = test; + attr.trigger.wait_value = value; + XSyncIntToValue (&attr.delta, 0); + + long flags = XSyncCACounter | XSyncCATestType | XSyncCAValue | XSyncCADelta; + if (*alarm) + XSyncChangeAlarm (ctx->dpy, *alarm, flags, &attr); + else + *alarm = XSyncCreateAlarm (ctx->dpy, flags, &attr); +} + +static void +on_x_alarm_notify (struct app_context *ctx, XSyncAlarmNotifyEvent *ev) +{ + if (ev->alarm == ctx->idle_alarm_inactive) + { + print_status ("User is inactive"); + + XSyncValue one, minus_one; + XSyncIntToValue (&one, 1); + + Bool overflow; + XSyncValueSubtract (&minus_one, ev->counter_value, one, &overflow); + + // Set an alarm for IDLETIME <= current_idletime - 1 + set_idle_alarm (ctx, &ctx->idle_alarm_active, + XSyncNegativeComparison, minus_one); + } + else if (ev->alarm == ctx->idle_alarm_active) + { + print_status ("User is active"); + set_idle_alarm (ctx, &ctx->idle_alarm_inactive, + XSyncPositiveComparison, ctx->idle_timeout); + } +} + +static void +on_x_ready (const struct pollfd *pfd, void *user_data) +{ + (void) pfd; + struct app_context *ctx = user_data; + + XEvent ev; + while (XPending (ctx->dpy)) + { + if (XNextEvent (ctx->dpy, &ev)) + exit_fatal ("XNextEvent returned non-zero"); + else if (ev.type == PropertyNotify) + on_x_property_notify (ctx, &ev.xproperty); + else if (ev.type == ctx->xsync_base_event_code + XSyncAlarmNotify) + on_x_alarm_notify (ctx, (XSyncAlarmNotifyEvent *) &ev); + } +} + +static XErrorHandler g_default_x_error_handler; + +static int +on_x_error (Display *dpy, XErrorEvent *ee) +{ + // This just is going to happen since those windows aren't ours + if (ee->error_code == BadWindow) + return 0; + + return g_default_x_error_handler (dpy, ee); +} + +static void +init_events (struct app_context *ctx) +{ + Window root = DefaultRootWindow (ctx->dpy); + XSelectInput (ctx->dpy, root, PropertyChangeMask); + XSync (ctx->dpy, False); + + g_default_x_error_handler = XSetErrorHandler (on_x_error); + + unsigned long n; + const char *timeout = str_map_find (&ctx->config, "idle_timeout"); + if (!xstrtoul (&n, timeout, 10) || !n || n > INT_MAX / 1000) + exit_fatal ("invalid value for the idle timeout"); + XSyncIntToValue (&ctx->idle_timeout, n * 1000); + + update_current_window (ctx); + set_idle_alarm (ctx, &ctx->idle_alarm_inactive, + XSyncPositiveComparison, ctx->idle_timeout); + + ctx->x_event.dispatcher = on_x_ready; + ctx->x_event.user_data = ctx; + poller_fd_set (&ctx->x_event, POLLIN); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +int +main (int argc, char *argv[]) +{ + g_log_message_real = log_message_custom; + + static const struct opt opts[] = + { + { 'd', "debug", NULL, 0, "run in debug mode" }, + { 'h', "help", NULL, 0, "display this help and exit" }, + { 'V', "version", NULL, 0, "output version information and exit" }, + { 'w', "write-default-cfg", "FILENAME", + OPT_OPTIONAL_ARG | OPT_LONG_ONLY, + "write a default configuration file and exit" }, + { 0, NULL, NULL, 0, NULL } + }; + + struct opt_handler oh; + opt_handler_init (&oh, argc, argv, opts, NULL, "Activity tracker."); + + int c; + while ((c = opt_handler_get (&oh)) != -1) + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + opt_handler_usage (&oh, stdout); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + case 'w': + call_simple_config_write_default (optarg, g_config_table); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + argc -= optind; + argv += optind; + + opt_handler_free (&oh); + + if (!setlocale (LC_CTYPE, "")) + exit_fatal ("cannot set locale"); + if (!XSupportsLocale ()) + exit_fatal ("locale not supported by Xlib"); + + struct app_context ctx; + app_context_init (&ctx); + + struct error *e = NULL; + if (!simple_config_update_from_file (&ctx.config, &e)) + exit_fatal ("%s", e->message); + + init_events (&ctx); + + ctx.running = true; + while (ctx.running) + poller_run (&ctx.poller); + + app_context_free (&ctx); + return 0; +} |