diff --git a/CMakeLists.txt b/CMakeLists.txt index 19bf901..2f6d3e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,19 +33,20 @@ pkg_check_modules (dependencies REQUIRED gtk+-3.0 sqlite3 x11 xext xextproto) # Precompile Vala sources include (ValaPrecompile) -set (config_path "${PROJECT_BINARY_DIR}/config.vala") -configure_file (${PROJECT_SOURCE_DIR}/config.vala.in "${config_path}") +set (config_path "${PROJECT_BINARY_DIR}/config.h") +configure_file (${PROJECT_SOURCE_DIR}/config.h.in "${config_path}") +include_directories ("${PROJECT_BINARY_DIR}") # I'm not sure what this was about, look at slovnik-gui for more comments; # seems to be so that symbols are exported for GModule to see -set (symbols_path "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.def") +set (symbols_path "${PROJECT_BINARY_DIR}/gui.def") -set (project_VALA_SOURCES ${config_path} ${PROJECT_NAME}.vala xext.vapi) +set (project_VALA_SOURCES gui.vala config.vapi) vala_precompile (${project_VALA_SOURCES} OUTPUTS project_VALA_C - HEADER ${PROJECT_NAME}.h + HEADER gui.h SYMBOLS ${symbols_path} - PACKAGES posix gmodule-2.0 gio-2.0 gio-unix-2.0 gtk+-3.0 gee-0.8 sqlite3 x11) + PACKAGES gmodule-2.0 gtk+-3.0 gee-0.8 sqlite3) # Include Vala sources as header files, so they appear in the IDE # but CMake doesn't try to compile them directly @@ -56,7 +57,7 @@ set (project_SOURCES ${project_VALA_SOURCES} ${project_VALA_C} ${symbols_path}) # Build the executable and install it include_directories (${dependencies_INCLUDE_DIRS}) link_directories (${dependencies_LIBRARY_DIRS}) -add_executable (${PROJECT_NAME} ${project_SOURCES}) +add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${project_SOURCES}) target_link_libraries (${PROJECT_NAME} ${dependencies_LIBRARIES}) install (TARGETS ${PROJECT_NAME} DESTINATION bin) diff --git a/README.adoc b/README.adoc index b002b2d..44b7735 100644 --- a/README.adoc +++ b/README.adoc @@ -43,9 +43,6 @@ Use https://git.janouch.name/p/wdmtg to report any bugs, request features, or submit pull requests. `git send-email` is tolerated. If you want to discuss the project, feel free to join me at ircs://irc.janouch.name, channel #dev. -The in-source dependency comments are there for the VIM Syntastic Vala plugin. -Vala just makes all sorts of things complicated but it's a necessary evil here. - Bitcoin donations are accepted at: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9 License diff --git a/config.h.in b/config.h.in new file mode 100644 index 0000000..32ca612 --- /dev/null +++ b/config.h.in @@ -0,0 +1,3 @@ +#define PROJECT_NAME "@CMAKE_PROJECT_NAME@" +#define PROJECT_VERSION "@project_VERSION@" +#define SHARE_DIR "@project_SHARE_DIR@" diff --git a/config.vala.in b/config.vala.in deleted file mode 100644 index 9a4b63b..0000000 --- a/config.vala.in +++ /dev/null @@ -1,8 +0,0 @@ -[CCode (cprefix = "", lower_case_cprefix = "")] -namespace Config -{ - public const string PROJECT_NAME = "@CMAKE_PROJECT_NAME@"; - public const string PROJECT_VERSION = "@project_VERSION@"; - public const string SHARE_DIR = "@project_SHARE_DIR@"; -} - diff --git a/config.vapi b/config.vapi new file mode 100644 index 0000000..c760a9c --- /dev/null +++ b/config.vapi @@ -0,0 +1,6 @@ +[CCode (cheader_filename = "config.h", cprefix = "", lower_case_cprefix = "")] +namespace Config { + public const string PROJECT_NAME; + public const string PROJECT_VERSION; + public const string SHARE_DIR; +} diff --git a/gui.vala b/gui.vala new file mode 100644 index 0000000..fe3bcb7 --- /dev/null +++ b/gui.vala @@ -0,0 +1,24 @@ +// +// gui.vala: activity tracker - GUI part +// +// Copyright (c) 2020, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// 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. +// + +public class WdmtgWindow : Gtk.Window { + private unowned Sqlite.Database db; + + public WdmtgWindow.with_db (Sqlite.Database db) { + this.db = db; + } +} diff --git a/meson.build b/meson.build index f1c97b1..62f58d0 100644 --- a/meson.build +++ b/meson.build @@ -7,8 +7,8 @@ conf = configuration_data() conf.set('CMAKE_PROJECT_NAME', meson.project_name()) conf.set('project_VERSION', meson.project_version()) configure_file( - input : 'config.vala.in', - output : 'config.vala', + input : 'config.h.in', + output : 'config.h', configuration : conf, ) @@ -20,18 +20,13 @@ dependencies = [ dependency('gee-0.8'), dependency('sqlite3'), dependency('x11'), - - # Only because of flock - meson.get_compiler('vala').find_library('posix'), - - # Ours dependency('xext'), dependency('xextproto'), ] -sources = files( - 'wdmtg.vala', - meson.current_build_dir() / 'config.vala', -) -executable('wdmtg', sources, - install : true, +gui = static_library('gui', 'gui.vala', 'config.vapi', + install : false, dependencies : dependencies) +executable('wdmtg', 'wdmtg.c', + install : true, + link_with : [gui], + dependencies : [dependencies]) diff --git a/wdmtg.c b/wdmtg.c new file mode 100644 index 0000000..d794c8b --- /dev/null +++ b/wdmtg.c @@ -0,0 +1,506 @@ +// +// wdmtg.c: activity tracker +// +// Copyright (c) 2016 - 2020, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// 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. +// + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "config.h" +#include "gui.h" + +// --- Utilities --------------------------------------------------------------- + +static void +exit_fatal(const gchar *format, ...) G_GNUC_PRINTF(1, 2); + +static void +exit_fatal(const gchar *format, ...) +{ + va_list ap; + va_start(ap, format); + + gchar *format_nl = g_strdup_printf("%s\n", format); + vfprintf(stderr, format_nl, ap); + free(format_nl); + + va_end(ap); + exit(EXIT_FAILURE); +} + +static GStrv +get_xdg_config_dirs(void) +{ + GPtrArray *paths = g_ptr_array_new(); + g_ptr_array_add(paths, g_strdup(g_get_user_config_dir())); + for (const gchar *const *sys = g_get_system_config_dirs(); *sys; sys++) + g_ptr_array_add(paths, g_strdup(*sys)); + g_ptr_array_add(paths, NULL); + return (GStrv) g_ptr_array_free(paths, false); +} + +// --- Globals ----------------------------------------------------------------- + +struct event { + gint64 timestamp; // When the event happened + gchar *title; // Current title at the time + gboolean idle; // Whether the user is idle +}; + +static void +event_free(struct event *self) +{ + g_free(self->title); + g_slice_free(struct event, self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct { + GAsyncQueue *queue; // Async queue of `struct event` +} g; + +struct { + Display *dpy; // X display handle + + Atom net_active_window; // _NET_ACTIVE_WINDOW + Atom net_wm_name; // _NET_WM_NAME + + // Window title tracking + + gchar *current_title; // Current window title or NULL + Window current_window; // Current window + gboolean current_idle; // Current idle status + + // 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 +} gen; + +// --- X helpers --------------------------------------------------------------- + +static XSyncCounter +get_counter(const char *name) +{ + int n; + XSyncSystemCounter *counters = XSyncListSystemCounters(gen.dpy, &n); + XSyncCounter counter = None; + while (n--) { + if (!strcmp(counters[n].name, name)) + counter = counters[n].counter; + } + XSyncFreeSystemCounterList(counters); + return counter; +} + +static char * +x_text_property_to_utf8(XTextProperty *prop) +{ + Atom utf8_string = XInternAtom(gen.dpy, "UTF8_STRING", true); + if (prop->encoding == utf8_string) + return g_strdup((char *) prop->value); + + int n = 0; + char **list = NULL; + if (XmbTextPropertyToTextList(gen.dpy, prop, &list, &n) >= Success + && n > 0 && *list) { + char *result = g_locale_to_utf8(*list, -1, NULL, NULL, NULL); + XFreeStringList(list); + return result; + } + return NULL; +} + +static char * +x_text_property(Window window, Atom atom) +{ + XTextProperty name; + XGetTextProperty(gen.dpy, window, &name, atom); + if (!name.value) + return NULL; + + char *result = x_text_property_to_utf8(&name); + XFree(name.value); + return result; +} + +// --- X error handling -------------------------------------------------------- + +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); +} + +// --- Application ------------------------------------------------------------- + +static char * +x_window_title(Window window) +{ + char *title; + if (!(title = x_text_property(window, gen.net_wm_name)) + && !(title = x_text_property(window, XA_WM_NAME))) + title = g_strdup("broken"); + return title; +} + +static bool +update_window_title(char *new_title) +{ + bool changed = !gen.current_title != !new_title + || (new_title && strcmp(gen.current_title, new_title)); + free(gen.current_title); + gen.current_title = new_title; + return changed; +} + +static void +push_event(void) { + struct event *event = g_slice_new0(struct event); + event->timestamp = g_get_real_time(); + event->title = g_strdup(gen.current_title); + event->idle = gen.current_idle; + g_async_queue_push(g.queue, event); +} + +static void +update_current_window(void) +{ + Window root = DefaultRootWindow(gen.dpy); + + Atom dummy_type; int dummy_format; unsigned long nitems, dummy_bytes; + unsigned char *p = NULL; + if (XGetWindowProperty(gen.dpy, root, gen.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 (gen.current_window != active_window && gen.current_window) + XSelectInput(gen.dpy, gen.current_window, 0); + + XSelectInput(gen.dpy, active_window, PropertyChangeMask); + new_title = x_window_title(active_window); + gen.current_window = active_window; + } + if (update_window_title(new_title)) { + printf("Window changed: %s\n", + gen.current_title ? gen.current_title : "(none)"); + push_event(); + } +} + +static void +on_x_property_notify(XPropertyEvent *ev) +{ + // This is from the EWMH specification, set by the window manager + if (ev->atom == gen.net_active_window) { + update_current_window(); + } else if (ev->window == gen.current_window && + ev->atom == gen.net_wm_name) { + if (update_window_title(x_window_title(ev->window))) { + printf("Title changed: %s\n", gen.current_title); + push_event(); + } + } +} + +static void +set_idle_alarm(XSyncAlarm *alarm, XSyncTestType test, XSyncValue value) +{ + XSyncAlarmAttributes attr; + attr.trigger.counter = gen.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(gen.dpy, *alarm, flags, &attr); + else + *alarm = XSyncCreateAlarm(gen.dpy, flags, &attr); +} + +static void +on_x_alarm_notify(XSyncAlarmNotifyEvent *ev) +{ + if (ev->alarm == gen.idle_alarm_inactive) { + printf("User is inactive\n"); + gen.current_idle = true; + push_event(); + + 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(&gen.idle_alarm_active, + XSyncNegativeComparison, minus_one); + } else if (ev->alarm == gen.idle_alarm_active) { + printf("User is active\n"); + gen.current_idle = false; + push_event(); + + set_idle_alarm(&gen.idle_alarm_inactive, + XSyncPositiveComparison, gen.idle_timeout); + } +} + +static gboolean +on_x_ready(G_GNUC_UNUSED gpointer user_data) +{ + XEvent ev; + while (XPending(gen.dpy)) { + if (XNextEvent(gen.dpy, &ev)) + exit_fatal("XNextEvent returned non-zero"); + else if (ev.type == PropertyNotify) + on_x_property_notify(&ev.xproperty); + else if (ev.type == gen.xsync_base_event_code + XSyncAlarmNotify) + on_x_alarm_notify((XSyncAlarmNotifyEvent *) &ev); + } + return G_SOURCE_CONTINUE; +} + +static void +generate_events(void) +{ + GIOChannel *channel = g_io_channel_unix_new(ConnectionNumber(gen.dpy)); + GSource *watch = g_io_create_watch(channel, G_IO_IN); + g_source_set_callback(watch, on_x_ready, NULL, NULL); + + GMainLoop *loop = + g_main_loop_new(g_main_context_get_thread_default(), false); + g_source_attach(watch, g_main_loop_get_context(loop)); + g_main_loop_run(loop); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +int +main(int argc, char *argv[]) +{ + gboolean show_version = false; + const GOptionEntry options[] = { + {"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, + &show_version, "output version information and exit", NULL}, + {}, + }; + + GError *error = NULL; + if (!gtk_init_with_args(&argc, &argv, " - activity tracker", + options, NULL, &error)) + exit_fatal("%s", error->message); + if (show_version) { + printf(PROJECT_NAME " " PROJECT_VERSION "\n"); + return 0; + } + + g.queue = g_async_queue_new_full((GDestroyNotify) event_free); + + if (!setlocale(LC_CTYPE, "")) + exit_fatal("cannot set locale"); + if (!XSupportsLocale()) + exit_fatal("locale not supported by Xlib"); + + XInitThreads(); + if (!(gen.dpy = XOpenDisplay(NULL))) + exit_fatal("cannot open display"); + + gen.net_active_window = XInternAtom(gen.dpy, "_NET_ACTIVE_WINDOW", true); + gen.net_wm_name = XInternAtom(gen.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 dummy; + if (!XSyncQueryExtension(gen.dpy, &gen.xsync_base_event_code, &dummy) + || !XSyncInitialize(gen.dpy, &dummy, &dummy)) + exit_fatal("cannot initialize XSync"); + + // The idle counter is not guaranteed to exist, only SERVERTIME is + if (!(gen.idle_counter = get_counter("IDLETIME"))) + exit_fatal("idle counter is missing"); + + Window root = DefaultRootWindow(gen.dpy); + XSelectInput(gen.dpy, root, PropertyChangeMask); + XSync(gen.dpy, False); + // TODO: what is the interaction with GTK+ here? + g_default_x_error_handler = XSetErrorHandler(on_x_error); + + GKeyFile *kf = g_key_file_new(); + gchar *subpath = g_build_filename(PROJECT_NAME, PROJECT_NAME ".conf", NULL); + GStrv dirs = get_xdg_config_dirs(); + + int timeout = 600; + if (g_key_file_load_from_dirs(kf, + subpath, (const gchar **) dirs, NULL, 0, NULL)) { + guint64 n = g_key_file_get_uint64(kf, "Settings", "idle_timeout", NULL); + if (n > 0 && n <= G_MAXINT / 1000) + timeout = n; + } + + g_strfreev(dirs); + g_free(subpath); + g_key_file_free(kf); + + XSyncIntToValue(&gen.idle_timeout, timeout * 1000); + update_current_window(); + set_idle_alarm(&gen.idle_alarm_inactive, + XSyncPositiveComparison, gen.idle_timeout); + + gchar *data_path = + g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL); + g_mkdir_with_parents(data_path, 0755); + + // TODO: try exclusivity/invocation either via DBus directly, + // or via GApplication or GtkApplication: + // - GtkApplication calls Gtk.init automatically during "startup" signal, + // Gtk.init doesn't get command line args + // - "inhibiting" makes no sense, it can't be used for mere delays + // - actually, the "query-end" signal + // - should check whether it tries to exit cleanly + // - what is the session manager, do I have it? + // - "register-session" looks useful + // - GTK+ keeps the application running as long as it has windows, + // though I want to keep it running forever + // - g_application_hold(), perhaps + // - so maybe just use GApplication, that will provide more control + + // Bind to a control socket, also ensuring only one instance is running + gchar *socket_path = g_build_filename(data_path, "socket", NULL); + + struct flock lock = + { + .l_type = F_WRLCK, + .l_start = 0, + .l_whence = SEEK_SET, + .l_len = 0, + }; + + gchar *lock_path = g_strdup_printf("%s.lock", socket_path); + int lock_fd = open(lock_path, O_RDWR | O_CREAT, 0644); + if (fcntl(lock_fd, F_SETLK, &lock)) + exit_fatal("failed to acquire lock: %s", strerror(errno)); + unlink(socket_path); + + int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd < 0) + exit_fatal("%s: %s", socket_path, strerror(errno)); + + struct sockaddr_un sun; + sun.sun_family = AF_UNIX; + strncpy(sun.sun_path, socket_path, sizeof sun.sun_path); + if (bind(socket_fd, (struct sockaddr *) &sun, sizeof sun)) + exit_fatal("%s: %s", socket_path, strerror(errno)); + if (listen(socket_fd, 10)) + exit_fatal("%s: %s", socket_path, strerror(errno)); + + sqlite3 *db = NULL; + gchar *db_path = g_build_filename(data_path, "db.sqlite", NULL); + int rc = sqlite3_open(db_path, &db); + if (rc != SQLITE_OK) + exit_fatal("%s: %s", db_path, sqlite3_errmsg(db)); + + // This shouldn't normally happen but external applications may decide + // to read things out, and mess with us. When that takes too long, we may + // a/ wait for it to finish, b/ start with a whole-database lock or, even + // more simply, c/ crash on the BUSY error. + sqlite3_busy_timeout(db, 1000); + + char *errmsg = NULL; + if ((rc = sqlite3_exec(db, "BEGIN", NULL, NULL, &errmsg))) + exit_fatal("%s: %s", db_path, errmsg); + + sqlite3_stmt *stmt = NULL; + if ((rc = sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &stmt, NULL))) + exit_fatal("%s: %s", db_path, sqlite3_errmsg(db)); + if ((rc = sqlite3_step(stmt)) != SQLITE_ROW || + sqlite3_data_count(stmt) != 1) + exit_fatal("%s: %s", db_path, "cannot retrieve user version"); + + int user_version = sqlite3_column_int(stmt, 0); + if (user_version == 0) { + if ((rc = sqlite3_exec(db, "CREATE TABLE events (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "timestamp INTEGER, " + "title TEXT, " + "idle BOOLEAN)", NULL, NULL, &errmsg))) + exit_fatal("%s: %s", db_path, errmsg); + if ((rc = sqlite3_exec(db, "PRAGMA user_version = 1", NULL, NULL, + &errmsg))) + exit_fatal("%s: %s", db_path, errmsg); + } else if (user_version != 1) { + exit_fatal("%s: unsupported DB version: %d", db_path, user_version); + } + if ((rc = sqlite3_exec(db, "COMMIT", NULL, NULL, &errmsg))) + exit_fatal("%s: %s", db_path, errmsg); + + g_free(db_path); + g_free(lock_path); + g_free(socket_path); + g_free(data_path); + + GThread *generator = + g_thread_new("generator", (GThreadFunc) generate_events, NULL); + + // TODO: somehow read events from the async queue + // TODO: how in the name of fuck would our custom source wake up a sleeping + // main loop? There is g_main_context_wakeup() but... + // - GWakeUp is internal, apparently + + // TODO: listen for connections on the control socket + + WdmtgWindow *window = wdmtg_window_new_with_db(db); + + gtk_main(); + g_thread_join(generator); + + free(gen.current_title); + XCloseDisplay(gen.dpy); + return 0; +} diff --git a/wdmtg.vala b/wdmtg.vala deleted file mode 100644 index c40ddd1..0000000 --- a/wdmtg.vala +++ /dev/null @@ -1,406 +0,0 @@ -// -// wdmtg.vala: activity tracker -// -// Copyright (c) 2016 - 2020, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// 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. -// -// vim: set sw=2 ts=2 sts=2 et tw=80: -// modules: x11 xext config -// vapidirs: . ../build build - -namespace Wdmtg { - -// --- Utilities --------------------------------------------------------------- - - void exit_fatal (string format, ...) { - stderr.vprintf ("fatal: " + format + "\n", va_list ()); - Process.exit (1); - } - - string[] get_xdg_config_dirs () { - string[] paths = { Environment.get_user_config_dir () }; - foreach (var system_path in Environment.get_system_config_dirs ()) - paths += system_path; - return paths; - } - -// --- Globals ----------------------------------------------------------------- - - X.Display dpy; ///< X display handle - int sync_base; ///< Sync extension base - - X.ID idle_counter; ///< XSync IDLETIME counter - X.Sync.Value idle_timeout; ///< User idle timeout - - X.ID idle_alarm_inactive; ///< User is inactive - X.ID idle_alarm_active; ///< User is active - - X.Atom net_active_window; ///< _NET_ACTIVE_WINDOW atom - X.Atom net_wm_name; ///< _NET_WM_NAME atom - - string? current_title; ///< Current window title - X.Window current_window; ///< Current window - bool current_idle = false; ///< Current idle status - - struct Event { - public int64 timestamp; ///< When the event happened - public string? title; ///< Current title at the time - public bool idle; ///< Whether the user is idle - } - - AsyncQueue queue; ///< Async queue - -// --- X helpers --------------------------------------------------------------- - - X.ID get_counter (string name) { - int n_counters = 0; - var counters = X.Sync.list_system_counters (dpy, out n_counters); - X.ID counter = X.None; - while (n_counters-- > 0) { - if (counters[n_counters].name == name) - counter = counters[n_counters].counter; - } - X.Sync.free_system_counter_list (counters); - return counter; - } - - string? x_text_property_to_utf8 (ref X.TextProperty prop) { - X.Atom utf8_string = dpy.intern_atom ("UTF8_STRING", true); - if (prop.encoding == utf8_string) - return (string) prop.value; - - int n = 0; - uint8 **list = null; - if (X.mb_text_property_to_text_list (dpy, ref prop, out list, out n) - >= X.Success && n > 0 && null != list[0]) { - var result = ((string) list[0]).locale_to_utf8 (-1, null, null); - X.free_string_list (list); - return result; - } - return null; - } - - string? x_text_property (X.Window window, X.Atom atom) { - X.TextProperty name; - X.get_text_property (dpy, window, out name, atom); - if (null == name.@value) - return null; - - string? result = x_text_property_to_utf8 (ref name); - X.free (name.@value); - return result; - } - -// --- X error handling -------------------------------------------------------- - - X.ErrorHandler default_x_error_handler; - - int on_x_error (X.Display dpy, X.ErrorEvent *ee) { - // This just is going to happen since those windows aren't ours - if (ee.error_code == X.ErrorCode.BAD_WINDOW) - return 0; - return default_x_error_handler (dpy, ee); - } - -// --- Application ------------------------------------------------------------- - - string x_window_title (X.Window window) { - string? title; - if (null == (title = x_text_property (window, net_wm_name)) - && null == (title = x_text_property (window, X.XA_WM_NAME))) - title = "broken"; - return title; - } - - bool update_window_title (string? new_title) { - bool changed = (null == current_title) != (null == new_title) - || current_title != new_title; - current_title = new_title; - return changed; - } - - void push_event () { - queue.push(Event () { - timestamp = get_real_time (), - title = current_title, - idle = current_idle - }); - } - - void update_current_window () { - var root = dpy.default_root_window (); - X.Atom dummy_type; int dummy_format; ulong nitems, dummy_bytes; - void *p = null; - if (dpy.get_window_property (root, net_active_window, - 0, 1, false, X.XA_WINDOW, out dummy_type, out dummy_format, - out nitems, out dummy_bytes, out p) != X.Success) - return; - - string? new_title = null; - if (0 != nitems) { - X.Window active_window = *(X.Window *) p; - X.free (p); - - if (current_window != active_window && X.None != current_window) - dpy.select_input (current_window, 0); - dpy.select_input (active_window, X.EventMask.PropertyChangeMask); - new_title = x_window_title (active_window); - current_window = active_window; - } - if (update_window_title (new_title)) { - stdout.printf ("Window changed: %s\n", - null != current_title ? current_title : "(none)"); - push_event (); - } - } - - void on_x_property_notify (X.PropertyEvent *xproperty) { - // This is from the EWMH specification, set by the window manager - if (xproperty.atom == net_active_window) - update_current_window (); - else if (xproperty.window == current_window - && xproperty.atom == net_wm_name) { - if (update_window_title (x_window_title (current_window))) { - stdout.printf ("Title changed: %s\n", current_title); - push_event (); - } - } - } - - void set_idle_alarm - (ref X.ID alarm, X.Sync.TestType test, X.Sync.Value @value) { - X.Sync.AlarmAttributes attr = {}; - attr.trigger.counter = idle_counter; - attr.trigger.test_type = test; - attr.trigger.wait_value = @value; - X.Sync.int_to_value (out attr.delta, 0); - - X.Sync.CA flags = X.Sync.CA.Counter | X.Sync.CA.TestType - | X.Sync.CA.Value | X.Sync.CA.Delta; - if (X.None != alarm) - X.Sync.change_alarm (dpy, alarm, flags, ref attr); - else - alarm = X.Sync.create_alarm (dpy, flags, ref attr); - } - - void on_x_alarm_notify (X.Sync.AlarmNotifyEvent *xalarm) { - if (xalarm.alarm == idle_alarm_inactive) { - stdout.printf ("User is inactive\n"); - current_idle = true; - push_event (); - - X.Sync.Value one, minus_one; - X.Sync.int_to_value (out one, 1); - - int overflow; - X.Sync.value_subtract - (out minus_one, xalarm.counter_value, one, out overflow); - - // Set an alarm for IDLETIME <= current_idletime - 1 - set_idle_alarm (ref idle_alarm_active, - X.Sync.TestType.NegativeComparison, minus_one); - } else if (xalarm.alarm == idle_alarm_inactive) { - stdout.printf ("User is active\n"); - current_idle = false; - push_event (); - - set_idle_alarm (ref idle_alarm_inactive, - X.Sync.TestType.PositiveComparison, idle_timeout); - } - } - - void generate_events () { - var channel = new IOChannel.unix_new (dpy.connection_number ()); - var watch = channel.create_watch (IOCondition.IN); - watch.set_callback (() => { - X.Event ev = {0}; - while (0 != dpy.pending ()) { - if (0 != dpy.next_event (ref ev)) { - exit_fatal ("XNextEvent returned non-zero"); - } else if (ev.type == X.EventType.PropertyNotify) { - on_x_property_notify (&ev.xproperty); - } else if (ev.type == sync_base + X.Sync.EventType.AlarmNotify) { - on_x_alarm_notify ((X.Sync.AlarmNotifyEvent *) (&ev)); - } - } - return true; - }); - - var loop = new MainLoop (MainContext.get_thread_default ()); - watch.attach (loop.get_context ()); - loop.run (); - } - - bool show_version; - const OptionEntry[] options = { - { "version", 'V', OptionFlags.IN_MAIN, OptionArg.NONE, ref show_version, - "output version information and exit" }, - { null } - }; - - public int main (string[] args) { - if (null == Intl.setlocale (GLib.LocaleCategory.CTYPE)) - exit_fatal ("cannot set locale"); - if (0 == X.supports_locale ()) - exit_fatal ("locale not supported by Xlib"); - - try { - Gtk.init_with_args (ref args, " - activity tracker", options, null); - } catch (OptionError e) { - exit_fatal ("option parsing failed: %s", e.message); - } catch (Error e) { - exit_fatal ("%s", e.message); - } - if (show_version) { - stdout.printf (Config.PROJECT_NAME + " " + Config.PROJECT_VERSION + "\n"); - return 0; - } - - queue = new AsyncQueue (); - - X.init_threads (); - if (null == (dpy = new X.Display ())) - exit_fatal ("cannot open display"); - - net_active_window = dpy.intern_atom ("_NET_ACTIVE_WINDOW", true); - net_wm_name = dpy.intern_atom ("_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 dummy; - if (0 == X.Sync.query_extension (dpy, out sync_base, out dummy) - || 0 == X.Sync.initialize (dpy, out dummy, out dummy)) - exit_fatal ("cannot initialize XSync"); - - // The idle counter is not guaranteed to exist, only SERVERTIME is - if (X.None == (idle_counter = get_counter ("IDLETIME"))) - exit_fatal ("idle counter is missing"); - - var root = dpy.default_root_window (); - dpy.select_input (root, X.EventMask.PropertyChangeMask); - X.sync (dpy, false); - default_x_error_handler = X.set_error_handler (on_x_error); - - int timeout = 600; // 10 minutes by default - try { - var kf = new KeyFile (); - kf.load_from_dirs (Config.PROJECT_NAME + Path.DIR_SEPARATOR_S - + Config.PROJECT_NAME + ".conf", get_xdg_config_dirs (), null, 0); - - var n = kf.get_uint64 ("Settings", "idle_timeout"); - if (0 != n && n <= int.MAX / 1000) - timeout = (int) n; - } catch (Error e) { - // Ignore errors this far, keeping the defaults - } - - X.Sync.int_to_value (out idle_timeout, timeout * 1000); - update_current_window (); - set_idle_alarm (ref idle_alarm_inactive, - X.Sync.TestType.PositiveComparison, idle_timeout); - - var data_path = Path.build_filename (Environment.get_user_data_dir (), - Config.PROJECT_NAME); - DirUtils.create_with_parents (data_path, 0755); - - // TODO: try exclusivity/invocation either via DBus directly, - // or via GApplication or GtkApplication: - // - GtkApplication calls Gtk.init automatically during "startup" signal, - // Gtk.init doesn't get command line args - // - "inhibiting" makes no sense, it can't be used for mere delays - // - actually, the "query-end" signal - // - should check whether it tries to exit cleanly - // - what is the session manager, do I have it? - // - "register-session" looks useful - // - GTK+ keeps the application running as long as it has windows, - // though I want to keep it running forever - // - g_application_hold(), perhaps - // - so maybe just use GApplication, that will provide more control - - // Bind to a control socket, also ensuring only one instance is running - var socket_path = Path.build_filename (data_path, "socket"); - - Posix.Flock fl = Posix.Flock () { - l_type = Posix.F_WRLCK, - l_start = 0, - l_whence = Posix.SEEK_SET, - l_len = 0 - }; - - var lk = FileStream.open (socket_path + ".lock", "w"); - if (Posix.fcntl (lk.fileno (), Posix.F_SETLK, &fl) < 0) - exit_fatal("failed to acquire lock: %s", Posix.errno.to_string ()); - FileUtils.unlink (socket_path); - - Socket socket; - try { - socket = new Socket (SocketFamily.UNIX, SocketType.STREAM, - SocketProtocol.DEFAULT); - socket.bind (new UnixSocketAddress (socket_path), true /* allow_reuse */); - socket.listen (); - } catch (Error e) { - exit_fatal ("%s: %s", socket_path, e.message); - } - - Sqlite.Database db; - var db_path = Path.build_filename (data_path, "db.sqlite"); - int rc = Sqlite.Database.open (db_path, out db); - if (rc != Sqlite.OK) - exit_fatal ("%s: %s", db_path, db.errmsg ()); - - // This shouldn't normally happen but external applications may decide - // to read things out, and mess with us. When that takes too long, we may - // a/ wait for it to finish, b/ start with a whole-database lock or, even - // more simply, c/ crash on the BUSY error. - db.busy_timeout (1000); - - string errmsg; - if ((rc = db.exec ("BEGIN", null, out errmsg)) != Sqlite.OK) - exit_fatal ("%s: %s", db_path, errmsg); - - Sqlite.Statement stmt; - if ((rc = db.prepare_v2 ("PRAGMA user_version", -1, out stmt, null)) - != Sqlite.OK) - exit_fatal ("%s: %s", db_path, db.errmsg ()); - if ((rc = stmt.step ()) != Sqlite.ROW || stmt.data_count () != 1) - exit_fatal ("%s: %s", db_path, "cannot retrieve user version"); - - var user_version = stmt.column_int (0); - if (user_version == 0) { - if ((rc = db.exec ("""CREATE TABLE events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp INTEGER, - title TEXT, - idle BOOLEAN - )""", null, out errmsg)) != Sqlite.OK) - exit_fatal ("%s: %s", db_path, errmsg); - - if ((rc = db.exec ("PRAGMA user_version = 1", null, out errmsg)) - != Sqlite.OK) - exit_fatal ("%s: %s", db_path, errmsg); - } else if (user_version != 1) { - exit_fatal ("%s: unsupported DB version: %d", db_path, user_version); - } - if ((rc = db.exec ("COMMIT", null, out errmsg)) != Sqlite.OK) - exit_fatal ("%s: %s", db_path, errmsg); - - var generator = new Thread ("generator", generate_events); - // TODO: somehow read events from the async queue - // TODO: listen for connections on the control socket - Gtk.main (); - generator.join (); - return 0; - } -} diff --git a/xext.deps b/xext.deps deleted file mode 100644 index e181da0..0000000 --- a/xext.deps +++ /dev/null @@ -1 +0,0 @@ -x11 diff --git a/xext.vapi b/xext.vapi index 69019b5..d5e2b49 100644 --- a/xext.vapi +++ b/xext.vapi @@ -1,252 +1 @@ -// -// xext.vapi: various extensions to the x11 vapi -// -// Copyright (c) 2016 - 2020, Přemysl Eric Janouch -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// 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. -// -// vim: set sw=2 ts=2 sts=2 et tw=80: -// modules: x11 - -namespace X { - [CCode (cname = "XErrorHandler", has_target = false)] - public delegate int ErrorHandler (Display dpy, ErrorEvent *ee); - [CCode (cname = "XSetErrorHandler")] - public ErrorHandler set_error_handler (ErrorHandler handler); - - // XXX: can we extend the Display class so that the argument goes away? - [CCode (cname = "XSync")] - public int sync (Display dpy, bool discard); - [CCode (cname = "XSupportsLocale")] - public int supports_locale (); - - [CCode (cname = "XTextProperty", has_type_id = false)] - public struct TextProperty { - // There is always a null byte at the end but it may also appear earlier - // depending on the other fields, so this is a bit misleading - [CCode (array_null_terminated = true)] - // Vala tries to g_free0() owned arrays, you still need to call XFree() - public unowned uint8[]? @value; - public Atom encoding; - public int format; - public ulong nitems; - } - [CCode (cname = "XGetTextProperty")] - public int get_text_property (Display dpy, - Window window, out TextProperty text_prop_return, Atom property); - - [CCode (cname = "XmbTextPropertyToTextList")] - public int mb_text_property_to_text_list (Display dpy, - ref TextProperty text_prop, - [CCode (type = "char ***")] out uint8 **list_return, out int count_return); - [CCode (cname = "XFreeStringList")] - public void free_string_list ([CCode (type = "char **")] uint8** list); -} - -namespace X { - [CCode (cprefix = "", cheader_filename = "X11/extensions/sync.h")] - namespace Sync { - [CCode (cprefix = "XSync", cname = "int", has_type_id = false)] - public enum EventType { - CounterNotify, - AlarmNotify - } - - [CCode (cprefix = "", cname = "int", has_type_id = false)] - public enum ErrorCode { - [CCode (cname = "XSyncBadCounter")] - BAD_COUNTER, - [CCode (cname = "XSyncBadAlarm")] - BAD_ALARM, - [CCode (cname = "XSyncBadFence")] - BAD_FENCE - } - - [CCode (cprefix = "XSyncCA", cname = "int")] - [Flags] - public enum CA { - Counter, - ValueType, - Value, - TestType, - Delta, - Events - } - - [CCode (cname = "XSyncValueType", cprefix = "XSync")] - public enum ValueType { - Absolute, - Relative - } - [CCode (cname = "XSyncTestType", cprefix = "XSync")] - public enum TestType { - PositiveTransition, - NegativeTransition, - PositiveComparison, - NegativeComparison - } - [CCode (cname = "XSyncAlarmState", cprefix = "XSyncAlarm")] - public enum AlarmState { - Active, - Inactive, - Destroyed - } - - [CCode (cname = "XSyncValue", has_type_id = false)] - [SimpleType] - public struct Value { - public int hi; - public uint lo; - } - - [CCode (cname = "XSyncIntToValue")] - public void int_to_value (out Value value, int v); - [CCode (cname = "XSyncIntsToValue")] - public void ints_to_value (out Value value, uint l, int h); - - [CCode (cname = "XSyncValueGreaterThan")] - public int value_greater_than (Value a, Value b); - [CCode (cname = "XSyncValueLessThan")] - public int value_less_than (Value a, Value b); - [CCode (cname = "XSyncValueGreaterOrEqual")] - public int value_greater_or_equal (Value a, Value b); - [CCode (cname = "XSyncValueLessOrEqual")] - public int value_less_or_equal (Value a, Value b); - [CCode (cname = "XSyncValueEqual")] - public int value_equal (Value a, Value b); - - [CCode (cname = "XSyncValueIsNegative")] - public int value_is_negative (Value a, Value b); - [CCode (cname = "XSyncValueIsZero")] - public int value_is_zero (Value a, Value b); - [CCode (cname = "XSyncValueIsPositive")] - public int value_is_positive (Value a, Value b); - - [CCode (cname = "XSyncValueLow32")] - public uint value_low32 (Value value); - [CCode (cname = "XSyncValueHigh32")] - public int value_high32 (Value value); - - [CCode (cname = "XSyncValueAdd")] - public void value_add - (out Value result, Value a, Value b, out int poverflow); - [CCode (cname = "XSyncValueSubtract")] - public void value_subtract - (out Value result, Value a, Value b, out int poverflow); - - [CCode (cname = "XSyncMaxValue")] - public void max_value (out Value pv); - [CCode (cname = "XSyncMinValue")] - public void min_value (out Value pv); - - [CCode (cname = "XSyncSystemCounter", has_type_id = false)] - public struct SystemCounter { - public string name; - public X.ID counter; - public Value resolution; - } - [CCode (cname = "XSyncTrigger", has_type_id = false)] - public struct Trigger { - public X.ID counter; - public ValueType value_type; - public Value wait_value; - public TestType test_type; - } - [CCode (cname = "XSyncWaitCondition", has_type_id = false)] - public struct WaitCondition { - public Trigger trigger; - public Value event_threshold; - } - [CCode (cname = "XSyncAlarmAttributes", has_type_id = false)] - public struct AlarmAttributes { - public Trigger trigger; - public Value delta; - public int events; - public AlarmState state; - } - - [CCode (cname = "XSyncCounterNotifyEvent", has_type_id = false)] - public struct CounterNotifyEvent { - // TODO: other fields - public X.ID counter; - public Value wait_value; - public Value counter_value; - } - [CCode (cname = "XSyncAlarmNotifyEvent", has_type_id = false)] - public struct AlarmNotifyEvent { - // TODO: other fields - public X.ID alarm; - public Value counter_value; - public Value alarm_value; - public AlarmState state; - } - - [CCode (cname = "XSyncQueryExtension")] - public X.Status query_extension (X.Display dpy, - out int event_base, out int error_base); - [CCode (cname = "XSyncInitialize")] - public X.Status initialize (X.Display dpy, - out int major_version, out int minor_version); - [CCode (cname = "XSyncListSystemCounters")] - public SystemCounter *list_system_counters (X.Display dpy, - out int n_counters); - [CCode (cname = "XSyncFreeSystemCounterList")] - public void free_system_counter_list (SystemCounter *counters); - - [CCode (cname = "XSyncCreateCounter")] - public X.ID create_counter (X.Display dpy, Value initial_value); - [CCode (cname = "XSyncSetCounter")] - public X.Status set_counter (X.Display dpy, X.ID counter, Value value); - [CCode (cname = "XSyncChangeCounter")] - public X.Status change_counter (X.Display dpy, X.ID counter, Value value); - [CCode (cname = "XSyncDestroyCounter")] - public X.Status destroy_counter (X.Display dpy, X.ID counter); - [CCode (cname = "XSyncQueryCounter")] - public X.Status query_counter (X.Display dpy, - X.ID counter, out Value value); - - [CCode (cname = "XSyncAwait")] - public X.Status await (X.Display dpy, - WaitCondition *wait_list, int n_conditions); - - [CCode (cname = "XSyncCreateAlarm")] - public X.ID create_alarm (X.Display dpy, - CA values_mask, ref AlarmAttributes values); - [CCode (cname = "XSyncDestroyAlarm")] - public X.Status destroy_alarm (X.Display dpy, X.ID alarm); - [CCode (cname = "XSyncQueryAlarm")] - public X.Status query_alarm (X.Display dpy, - X.ID alarm, out AlarmAttributes values_return); - [CCode (cname = "XSyncChangeAlarm")] - public X.Status change_alarm (X.Display dpy, - X.ID alarm, CA values_mask, ref AlarmAttributes values); - - [CCode (cname = "XSyncSetPriority")] - public X.Status set_priority (X.Display dpy, X.ID alarm, int priority); - [CCode (cname = "XSyncGetPriority")] - public X.Status get_priority (X.Display dpy, X.ID alarm, out int priority); - - [CCode (cname = "XSyncCreateFence")] - public X.ID create_fence (X.Display dpy, - X.Drawable d, int initially_triggered); - [CCode (cname = "XSyncTriggerFence")] - public int trigger_fence (X.Display dpy, X.ID fence); - [CCode (cname = "XSyncResetFence")] - public int reset_fence (X.Display dpy, X.ID fence); - [CCode (cname = "XSyncDestroyFence")] - public int destroy_fence (X.Display dpy, X.ID fence); - [CCode (cname = "XSyncQueryFence")] - public int query_fence (X.Display dpy, X.ID fence, out int triggered); - [CCode (cname = "XSyncAwaitFence")] - public int await_fence (X.Display dpy, X.ID *fence_list, int n_fences); - } -} +// https://github.com/mesonbuild/meson/issues/1195