From 3482ee66a394f1afa7566db353cfb144e7ff1fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Fri, 2 Oct 2020 01:31:27 +0200 Subject: [PATCH] Watch changes of WM_CLASS There may be some interesting information in there. Sometimes it may be hard to identify applications by their title. --- wdmtg.c | 61 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/wdmtg.c b/wdmtg.c index 9d166eb..93aadfc 100644 --- a/wdmtg.c +++ b/wdmtg.c @@ -86,6 +86,7 @@ latin1_to_utf8(const gchar *latin1, gsize len) struct event { gint64 timestamp; // When the event happened gchar *title; // Current title at the time + gchar *class; // Current class at the time gboolean idle; // Whether the user is idle }; @@ -93,6 +94,7 @@ static void event_free(struct event *self) { g_free(self->title); + g_free(self->class); g_slice_free(struct event, self); } @@ -117,6 +119,7 @@ struct { // Window title tracking gchar *current_title; // Current window title or NULL + gchar *current_class; // Current window class or NULL xcb_window_t current_window; // Current window gboolean current_idle; // Current idle status @@ -286,6 +289,7 @@ 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->class = g_strdup(gen.current_class); event->idle = gen.current_idle; g_async_queue_push(g.queue, event); @@ -313,6 +317,33 @@ update_window_title(char *new_title) return changed; } +static char * +x_window_class(xcb_window_t window) +{ + GString *title; + if (!(title = x_text_property(window, XCB_ATOM_WM_CLASS))) + return NULL; + + // First is an "instance name", followed by a NUL and a "class name". + // Strongly prefer Firefox/Thunderbird over Navigator/Mail. + size_t skip = strlen(title->str); + if (++skip >= title->len) + return g_string_free(title, true); + + g_string_erase(title, 0, skip); + return g_string_free(title, false); +} + +static bool +update_window_class(char *new_class) +{ + bool changed = !gen.current_class != !new_class + || (new_class && strcmp(gen.current_class, new_class)); + free(gen.current_class); + gen.current_class = new_class; + return changed; +} + static void update_current_window(void) { @@ -322,14 +353,8 @@ update_current_window(void) if (!gpr) return; - // TODO: also consider watching XCB_ATOM_WM_CLASS - // - the first one is an "Instance name", the second one a "Class name" - // - don't know which one to pick, though probably the second one, - // since the instance name is Navigator/Mail for Firefox/Thunderbird - // - they are separated by a NUL character - // - it's not worth to watch for changes of this property - char *new_title = NULL; + char *new_class = NULL; if (xcb_get_property_value_length(gpr)) { xcb_window_t active_window = *(xcb_window_t *) xcb_get_property_value(gpr); @@ -344,11 +369,14 @@ update_current_window(void) active_window, XCB_CW_EVENT_MASK, enable); new_title = x_window_title(active_window); + new_class = x_window_class(active_window); gen.current_window = active_window; } - free(gpr); - if (update_window_title(new_title)) + + // We need to absorb both pointers, so we need a bitwise OR + if (update_window_title(new_title) | + update_window_class(new_class)) push_event(); } @@ -555,14 +583,18 @@ on_queue_incoming(G_GNUC_UNUSED gpointer user_data) // in a transaction at once (the amount of dequeues here) struct event *event = NULL; while ((event = g_async_queue_try_pop(g.queue))) { - printf("Event: ts: %ld, title: %s, idle: %d\n", - event->timestamp, event->title ?: "(none)", event->idle); + printf("Event: ts: %ld, title: %s, class: %s, idle: %d\n", + event->timestamp, event->title ?: "(none)", + event->class ?: "(none)", event->idle); if ((rc = sqlite3_bind_int64(g.add_event, 1, event->timestamp)) || // FIXME: it will fail on NULL titles (rc = sqlite3_bind_text(g.add_event, 2, event->title, -1, SQLITE_STATIC)) || - (rc = sqlite3_bind_int(g.add_event, 3, event->idle))) + // FIXME: it will fail on NULL classes + (rc = sqlite3_bind_text(g.add_event, 3, event->class, -1, + SQLITE_STATIC)) || + (rc = sqlite3_bind_int(g.add_event, 4, event->idle))) g_printerr("DB bind error: %s\n", sqlite3_errmsg(g.db)); if (((rc = sqlite3_step(g.add_event)) && rc != SQLITE_DONE) || @@ -612,6 +644,7 @@ database_init(const gchar *db_path) "id INTEGER PRIMARY KEY AUTOINCREMENT, " "timestamp INTEGER, " "title TEXT, " + "class TEXT, " "idle BOOLEAN)", NULL, NULL, &errmsg))) exit_fatal("%s: %s", db_path, errmsg); if ((rc = sqlite3_exec(db, "PRAGMA user_version = 1", NULL, NULL, @@ -624,8 +657,8 @@ database_init(const gchar *db_path) exit_fatal("%s: %s", db_path, errmsg); if ((rc = sqlite3_prepare_v2(db, - "INSERT INTO events (timestamp, title, idle) VALUES (?, ?, ?)", -1, - &g.add_event, NULL))) + "INSERT INTO events (timestamp, title, class, idle) " + "VALUES (?, ?, ?, ?)", -1, &g.add_event, NULL))) exit_fatal("%s: %s", db_path, sqlite3_errmsg(db)); return db; }