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 2.8.12)
|
||||
cmake_minimum_required (VERSION 3.0)
|
||||
project (wdmtg VERSION 0.1.0 LANGUAGES C)
|
||||
|
||||
# Vala really sucks at producing good C code
|
||||
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
|
||||
set (CMAKE_C_FLAGS_RELEASE
|
||||
"${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
|
||||
option (OPTION_NOINSTALL "Only for developers; work without installing" OFF)
|
||||
|
||||
# Version
|
||||
set (project_VERSION "0.1.0")
|
||||
|
||||
# Set some variables
|
||||
if (OPTION_NOINSTALL)
|
||||
set (project_SHARE_DIR ${PROJECT_SOURCE_DIR}/share)
|
||||
elseif (WIN32)
|
||||
set (project_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_INSTALL_SHARE_DIR share/${PROJECT_NAME})
|
||||
endif (OPTION_NOINSTALL)
|
||||
endif ()
|
||||
|
||||
# Gather package information
|
||||
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_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
|
||||
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
|
||||
set (CPACK_PACKAGE_VERSION ${project_VERSION})
|
||||
set (CPACK_GENERATOR "TGZ;ZIP")
|
||||
set (CPACK_PACKAGE_FILE_NAME
|
||||
"${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}")
|
||||
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
|
||||
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#define PROJECT_NAME "@CMAKE_PROJECT_NAME@"
|
||||
#define PROJECT_VERSION "@project_VERSION@"
|
||||
#define PROJECT_NAME "@PROJECT_NAME@"
|
||||
#define PROJECT_VERSION "@PROJECT_VERSION@"
|
||||
#define SHARE_DIR "@project_SHARE_DIR@"
|
||||
|
||||
@@ -4,8 +4,8 @@ add_project_arguments(['--vapidir', meson.current_source_dir()],
|
||||
language: 'vala')
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set('CMAKE_PROJECT_NAME', meson.project_name())
|
||||
conf.set('project_VERSION', meson.project_version())
|
||||
conf.set('PROJECT_NAME', meson.project_name())
|
||||
conf.set('PROJECT_VERSION', meson.project_version())
|
||||
configure_file(
|
||||
input : 'config.h.in',
|
||||
output : 'config.h',
|
||||
|
||||
89
wdmtg.c
89
wdmtg.c
@@ -15,10 +15,6 @@
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <glib.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
@@ -28,6 +24,10 @@
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <glib.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/sync.h>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -299,7 +303,7 @@ x_window_title(xcb_window_t window)
|
||||
GString *title;
|
||||
if (!(title = x_text_property(window, gen.atom_net_wm_name))
|
||||
&& !(title = x_text_property(window, XCB_ATOM_WM_NAME)))
|
||||
return g_strdup("broken");
|
||||
return g_strdup("");
|
||||
return g_string_free(title, false);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -495,8 +523,6 @@ generator_init(void)
|
||||
|
||||
const uint32_t values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE };
|
||||
(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();
|
||||
gchar *subpath = g_build_filename(PROJECT_NAME, PROJECT_NAME ".conf", NULL);
|
||||
@@ -514,13 +540,21 @@ generator_init(void)
|
||||
g_free(subpath);
|
||||
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;
|
||||
gen.idle_timeout.hi = timeout_ms >> 32;
|
||||
gen.idle_timeout.lo = timeout_ms;
|
||||
|
||||
update_current_window();
|
||||
set_idle_alarm(&gen.idle_alarm_inactive,
|
||||
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
|
||||
@@ -555,14 +589,16 @@ 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)))
|
||||
(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 +648,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 +661,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;
|
||||
}
|
||||
@@ -707,7 +744,9 @@ main(int argc, char *argv[])
|
||||
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
|
||||
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);
|
||||
int socket_fd = socket_init(socket_path);
|
||||
g_free(socket_path);
|
||||
|
||||
Reference in New Issue
Block a user