Compare commits

..

9 Commits

Author SHA1 Message Date
d46305d7ab CMakeLists.txt: omit end{if,foreach} expressions
Their usefulness was almost negative.
2020-10-29 16:09:31 +01:00
7edd9720cd Bump minimum CMake version to 3.0
A nice, round number.
2020-10-26 23:28:11 +01:00
ab5ca0cf8b Elaborate on avoiding XDG_RUNTIME_DIR 2020-10-26 23:09:23 +01:00
f699b89dad Reorder headers 2020-10-02 02:08:39 +02:00
9244d2b657 Write a start marker to the DB event table 2020-10-02 01:55:46 +02:00
4302fc4baf Use an empty string rather than "broken"
If we fail to retrieve the title, then there is no title,
though this doesn't mean the same as "no window",
for which we have NULL.
2020-10-02 01:50:37 +02:00
764dbaa126 Nullify a NULL concern
sqlite3_bind_text() is documented to bind NULL.
2020-10-02 01:37:08 +02:00
7d4695d8bd Ensure the inactivity alarm is launched on startup
We forgot to flush.
2020-10-02 01:32:19 +02:00
3482ee66a3 Watch changes of WM_CLASS
There may be some interesting information in there.
Sometimes it may be hard to identify applications by their title.
2020-10-02 01:31:46 +02:00
4 changed files with 76 additions and 42 deletions

View File

@@ -1,28 +1,25 @@
project (wdmtg C) cmake_minimum_required (VERSION 3.0)
cmake_minimum_required (VERSION 2.8.12) project (wdmtg VERSION 0.1.0 LANGUAGES C)
# Vala really sucks at producing good C code # Vala really sucks at producing good C code
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
set (CMAKE_C_FLAGS_RELEASE set (CMAKE_C_FLAGS_RELEASE
"${CMAKE_C_FLAGS_RELEASE} -Wno-ignored-qualifiers -Wno-incompatible-pointer-types") "${CMAKE_C_FLAGS_RELEASE} -Wno-ignored-qualifiers -Wno-incompatible-pointer-types")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) endif ()
# Options # Options
option (OPTION_NOINSTALL "Only for developers; work without installing" OFF) option (OPTION_NOINSTALL "Only for developers; work without installing" OFF)
# Version
set (project_VERSION "0.1.0")
# Set some variables # Set some variables
if (OPTION_NOINSTALL) if (OPTION_NOINSTALL)
set (project_SHARE_DIR ${PROJECT_SOURCE_DIR}/share) set (project_SHARE_DIR ${PROJECT_SOURCE_DIR}/share)
elseif (WIN32) elseif (WIN32)
set (project_SHARE_DIR ../share) set (project_SHARE_DIR ../share)
set (project_INSTALL_SHARE_DIR share) set (project_INSTALL_SHARE_DIR share)
else (OPTION_NOINSTALL) else ()
set (project_SHARE_DIR ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}) set (project_SHARE_DIR ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME})
set (project_INSTALL_SHARE_DIR share/${PROJECT_NAME}) set (project_INSTALL_SHARE_DIR share/${PROJECT_NAME})
endif (OPTION_NOINSTALL) endif ()
# Gather package information # Gather package information
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
@@ -68,14 +65,12 @@ set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Activity tracker")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch") set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>") set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_PACKAGE_VERSION ${project_VERSION})
set (CPACK_GENERATOR "TGZ;ZIP") set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") "${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}") set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP") set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/build;/\\\\.git") set (CPACK_SOURCE_IGNORE_FILES "/build;/\\\\.git")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}") set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
include (CPack) include (CPack)

View File

@@ -1,3 +1,3 @@
#define PROJECT_NAME "@CMAKE_PROJECT_NAME@" #define PROJECT_NAME "@PROJECT_NAME@"
#define PROJECT_VERSION "@project_VERSION@" #define PROJECT_VERSION "@PROJECT_VERSION@"
#define SHARE_DIR "@project_SHARE_DIR@" #define SHARE_DIR "@project_SHARE_DIR@"

View File

@@ -4,8 +4,8 @@ add_project_arguments(['--vapidir', meson.current_source_dir()],
language: 'vala') language: 'vala')
conf = configuration_data() conf = configuration_data()
conf.set('CMAKE_PROJECT_NAME', meson.project_name()) conf.set('PROJECT_NAME', meson.project_name())
conf.set('project_VERSION', meson.project_version()) conf.set('PROJECT_VERSION', meson.project_version())
configure_file( configure_file(
input : 'config.h.in', input : 'config.h.in',
output : 'config.h', output : 'config.h',

89
wdmtg.c
View File

@@ -15,10 +15,6 @@
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// //
#include <gtk/gtk.h>
#include <glib.h>
#include <sqlite3.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <stdarg.h> #include <stdarg.h>
@@ -28,6 +24,10 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h> #include <sys/un.h>
#include <gtk/gtk.h>
#include <glib.h>
#include <sqlite3.h>
#include <xcb/xcb.h> #include <xcb/xcb.h>
#include <xcb/sync.h> #include <xcb/sync.h>
@@ -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);
@@ -299,7 +303,7 @@ x_window_title(xcb_window_t window)
GString *title; GString *title;
if (!(title = x_text_property(window, gen.atom_net_wm_name)) if (!(title = x_text_property(window, gen.atom_net_wm_name))
&& !(title = x_text_property(window, XCB_ATOM_WM_NAME))) && !(title = x_text_property(window, XCB_ATOM_WM_NAME)))
return g_strdup("broken"); return g_strdup("");
return g_string_free(title, false); return g_string_free(title, false);
} }
@@ -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();
} }
@@ -495,8 +523,6 @@ generator_init(void)
const uint32_t values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE }; const uint32_t values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE };
(void) xcb_change_window_attributes(gen.X, root, XCB_CW_EVENT_MASK, values); (void) xcb_change_window_attributes(gen.X, root, XCB_CW_EVENT_MASK, values);
(void) xcb_flush(gen.X);
// TODO: how are XCB errors handled? What if the last xcb_flush() fails?
GKeyFile *kf = g_key_file_new(); GKeyFile *kf = g_key_file_new();
gchar *subpath = g_build_filename(PROJECT_NAME, PROJECT_NAME ".conf", NULL); gchar *subpath = g_build_filename(PROJECT_NAME, PROJECT_NAME ".conf", NULL);
@@ -514,13 +540,21 @@ generator_init(void)
g_free(subpath); g_free(subpath);
g_key_file_free(kf); g_key_file_free(kf);
// Write a start marker so that we can reliably detect interruptions
struct event *event = g_slice_new0(struct event);
event->timestamp = -1;
g_async_queue_push(g.queue, event);
update_current_window();
gint64 timeout_ms = timeout * 1000; gint64 timeout_ms = timeout * 1000;
gen.idle_timeout.hi = timeout_ms >> 32; gen.idle_timeout.hi = timeout_ms >> 32;
gen.idle_timeout.lo = timeout_ms; gen.idle_timeout.lo = timeout_ms;
update_current_window();
set_idle_alarm(&gen.idle_alarm_inactive, set_idle_alarm(&gen.idle_alarm_inactive,
XCB_SYNC_TESTTYPE_POSITIVE_COMPARISON, gen.idle_timeout); XCB_SYNC_TESTTYPE_POSITIVE_COMPARISON, gen.idle_timeout);
(void) xcb_flush(gen.X);
// TODO: how are XCB errors handled? What if the last xcb_flush() fails?
} }
static void static void
@@ -555,14 +589,16 @@ 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
(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))) (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 +648,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 +661,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;
} }
@@ -707,7 +744,9 @@ main(int argc, char *argv[])
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL); g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
g_mkdir_with_parents(data_path, 0755); g_mkdir_with_parents(data_path, 0755);
// Bind to a control socket, also ensuring only one instance is running // Bind to a control socket, also ensuring only one instance is running.
// We're intentionally not using XDG_RUNTIME_DIR so that what is effectively
// the database lock is right next to the database.
gchar *socket_path = g_build_filename(data_path, "socket", NULL); gchar *socket_path = g_build_filename(data_path, "socket", NULL);
int socket_fd = socket_init(socket_path); int socket_fd = socket_init(socket_path);
g_free(socket_path); g_free(socket_path);