Compare commits
9 Commits
86b0579cb7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
d46305d7ab
|
|||
|
7edd9720cd
|
|||
|
ab5ca0cf8b
|
|||
|
f699b89dad
|
|||
|
9244d2b657
|
|||
|
4302fc4baf
|
|||
|
764dbaa126
|
|||
|
7d4695d8bd
|
|||
|
3482ee66a3
|
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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@"
|
||||||
|
|||||||
@@ -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
89
wdmtg.c
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user