Watch changes of WM_CLASS

There may be some interesting information in there.
Sometimes it may be hard to identify applications by their title.
This commit is contained in:
Přemysl Eric Janouch 2020-10-02 01:31:27 +02:00
parent 86b0579cb7
commit 3482ee66a3
Signed by: p
GPG Key ID: A0420B94F92B9493
1 changed files with 47 additions and 14 deletions

61
wdmtg.c
View File

@ -86,6 +86,7 @@ latin1_to_utf8(const gchar *latin1, gsize len)
struct event { struct event {
gint64 timestamp; // When the event happened gint64 timestamp; // When the event happened
gchar *title; // Current title at the time gchar *title; // Current title at the time
gchar *class; // Current class at the time
gboolean idle; // Whether the user is idle gboolean idle; // Whether the user is idle
}; };
@ -93,6 +94,7 @@ static void
event_free(struct event *self) event_free(struct event *self)
{ {
g_free(self->title); g_free(self->title);
g_free(self->class);
g_slice_free(struct event, self); g_slice_free(struct event, self);
} }
@ -117,6 +119,7 @@ struct {
// Window title tracking // Window title tracking
gchar *current_title; // Current window title or NULL gchar *current_title; // Current window title or NULL
gchar *current_class; // Current window class or NULL
xcb_window_t current_window; // Current window xcb_window_t current_window; // Current window
gboolean current_idle; // Current idle status gboolean current_idle; // Current idle status
@ -286,6 +289,7 @@ push_event(void) {
struct event *event = g_slice_new0(struct event); struct event *event = g_slice_new0(struct event);
event->timestamp = g_get_real_time(); event->timestamp = g_get_real_time();
event->title = g_strdup(gen.current_title); event->title = g_strdup(gen.current_title);
event->class = g_strdup(gen.current_class);
event->idle = gen.current_idle; event->idle = gen.current_idle;
g_async_queue_push(g.queue, event); g_async_queue_push(g.queue, event);
@ -313,6 +317,33 @@ update_window_title(char *new_title)
return changed; 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 static void
update_current_window(void) update_current_window(void)
{ {
@ -322,14 +353,8 @@ update_current_window(void)
if (!gpr) if (!gpr)
return; 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_title = NULL;
char *new_class = NULL;
if (xcb_get_property_value_length(gpr)) { if (xcb_get_property_value_length(gpr)) {
xcb_window_t active_window = xcb_window_t active_window =
*(xcb_window_t *) xcb_get_property_value(gpr); *(xcb_window_t *) xcb_get_property_value(gpr);
@ -344,11 +369,14 @@ update_current_window(void)
active_window, XCB_CW_EVENT_MASK, enable); active_window, XCB_CW_EVENT_MASK, enable);
new_title = x_window_title(active_window); new_title = x_window_title(active_window);
new_class = x_window_class(active_window);
gen.current_window = active_window; gen.current_window = active_window;
} }
free(gpr); 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(); 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) // in a transaction at once (the amount of dequeues here)
struct event *event = NULL; struct event *event = NULL;
while ((event = g_async_queue_try_pop(g.queue))) { while ((event = g_async_queue_try_pop(g.queue))) {
printf("Event: ts: %ld, title: %s, idle: %d\n", printf("Event: ts: %ld, title: %s, class: %s, idle: %d\n",
event->timestamp, event->title ?: "(none)", event->idle); event->timestamp, event->title ?: "(none)",
event->class ?: "(none)", event->idle);
if ((rc = sqlite3_bind_int64(g.add_event, 1, event->timestamp)) || if ((rc = sqlite3_bind_int64(g.add_event, 1, event->timestamp)) ||
// FIXME: it will fail on NULL titles // FIXME: it will fail on NULL titles
(rc = sqlite3_bind_text(g.add_event, 2, event->title, -1, (rc = sqlite3_bind_text(g.add_event, 2, event->title, -1,
SQLITE_STATIC)) || 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)); g_printerr("DB bind error: %s\n", sqlite3_errmsg(g.db));
if (((rc = sqlite3_step(g.add_event)) && rc != SQLITE_DONE) || 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, " "id INTEGER PRIMARY KEY AUTOINCREMENT, "
"timestamp INTEGER, " "timestamp INTEGER, "
"title TEXT, " "title TEXT, "
"class TEXT, "
"idle BOOLEAN)", NULL, NULL, &errmsg))) "idle BOOLEAN)", NULL, NULL, &errmsg)))
exit_fatal("%s: %s", db_path, errmsg); exit_fatal("%s: %s", db_path, errmsg);
if ((rc = sqlite3_exec(db, "PRAGMA user_version = 1", NULL, NULL, 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); exit_fatal("%s: %s", db_path, errmsg);
if ((rc = sqlite3_prepare_v2(db, if ((rc = sqlite3_prepare_v2(db,
"INSERT INTO events (timestamp, title, idle) VALUES (?, ?, ?)", -1, "INSERT INTO events (timestamp, title, class, idle) "
&g.add_event, NULL))) "VALUES (?, ?, ?, ?)", -1, &g.add_event, NULL)))
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db)); exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
return db; return db;
} }