Convert main source file from Vala to C
Bindings are incredible pain, this will be much easier.
This commit is contained in:
parent
0d35950715
commit
60bfaa1a97
|
@ -33,19 +33,20 @@ pkg_check_modules (dependencies REQUIRED gtk+-3.0 sqlite3 x11 xext xextproto)
|
||||||
# Precompile Vala sources
|
# Precompile Vala sources
|
||||||
include (ValaPrecompile)
|
include (ValaPrecompile)
|
||||||
|
|
||||||
set (config_path "${PROJECT_BINARY_DIR}/config.vala")
|
set (config_path "${PROJECT_BINARY_DIR}/config.h")
|
||||||
configure_file (${PROJECT_SOURCE_DIR}/config.vala.in "${config_path}")
|
configure_file (${PROJECT_SOURCE_DIR}/config.h.in "${config_path}")
|
||||||
|
include_directories ("${PROJECT_BINARY_DIR}")
|
||||||
|
|
||||||
# I'm not sure what this was about, look at slovnik-gui for more comments;
|
# I'm not sure what this was about, look at slovnik-gui for more comments;
|
||||||
# seems to be so that symbols are exported for GModule to see
|
# seems to be so that symbols are exported for GModule to see
|
||||||
set (symbols_path "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.def")
|
set (symbols_path "${PROJECT_BINARY_DIR}/gui.def")
|
||||||
|
|
||||||
set (project_VALA_SOURCES ${config_path} ${PROJECT_NAME}.vala xext.vapi)
|
set (project_VALA_SOURCES gui.vala config.vapi)
|
||||||
vala_precompile (${project_VALA_SOURCES}
|
vala_precompile (${project_VALA_SOURCES}
|
||||||
OUTPUTS project_VALA_C
|
OUTPUTS project_VALA_C
|
||||||
HEADER ${PROJECT_NAME}.h
|
HEADER gui.h
|
||||||
SYMBOLS ${symbols_path}
|
SYMBOLS ${symbols_path}
|
||||||
PACKAGES posix gmodule-2.0 gio-2.0 gio-unix-2.0 gtk+-3.0 gee-0.8 sqlite3 x11)
|
PACKAGES gmodule-2.0 gtk+-3.0 gee-0.8 sqlite3)
|
||||||
|
|
||||||
# Include Vala sources as header files, so they appear in the IDE
|
# Include Vala sources as header files, so they appear in the IDE
|
||||||
# but CMake doesn't try to compile them directly
|
# but CMake doesn't try to compile them directly
|
||||||
|
@ -56,7 +57,7 @@ set (project_SOURCES ${project_VALA_SOURCES} ${project_VALA_C} ${symbols_path})
|
||||||
# Build the executable and install it
|
# Build the executable and install it
|
||||||
include_directories (${dependencies_INCLUDE_DIRS})
|
include_directories (${dependencies_INCLUDE_DIRS})
|
||||||
link_directories (${dependencies_LIBRARY_DIRS})
|
link_directories (${dependencies_LIBRARY_DIRS})
|
||||||
add_executable (${PROJECT_NAME} ${project_SOURCES})
|
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${project_SOURCES})
|
||||||
target_link_libraries (${PROJECT_NAME} ${dependencies_LIBRARIES})
|
target_link_libraries (${PROJECT_NAME} ${dependencies_LIBRARIES})
|
||||||
|
|
||||||
install (TARGETS ${PROJECT_NAME} DESTINATION bin)
|
install (TARGETS ${PROJECT_NAME} DESTINATION bin)
|
||||||
|
|
|
@ -43,9 +43,6 @@ Use https://git.janouch.name/p/wdmtg to report any bugs, request features,
|
||||||
or submit pull requests. `git send-email` is tolerated. If you want to discuss
|
or submit pull requests. `git send-email` is tolerated. If you want to discuss
|
||||||
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.
|
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.
|
||||||
|
|
||||||
The in-source dependency comments are there for the VIM Syntastic Vala plugin.
|
|
||||||
Vala just makes all sorts of things complicated but it's a necessary evil here.
|
|
||||||
|
|
||||||
Bitcoin donations are accepted at: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9
|
Bitcoin donations are accepted at: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9
|
||||||
|
|
||||||
License
|
License
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#define PROJECT_NAME "@CMAKE_PROJECT_NAME@"
|
||||||
|
#define PROJECT_VERSION "@project_VERSION@"
|
||||||
|
#define SHARE_DIR "@project_SHARE_DIR@"
|
|
@ -1,8 +0,0 @@
|
||||||
[CCode (cprefix = "", lower_case_cprefix = "")]
|
|
||||||
namespace Config
|
|
||||||
{
|
|
||||||
public const string PROJECT_NAME = "@CMAKE_PROJECT_NAME@";
|
|
||||||
public const string PROJECT_VERSION = "@project_VERSION@";
|
|
||||||
public const string SHARE_DIR = "@project_SHARE_DIR@";
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
[CCode (cheader_filename = "config.h", cprefix = "", lower_case_cprefix = "")]
|
||||||
|
namespace Config {
|
||||||
|
public const string PROJECT_NAME;
|
||||||
|
public const string PROJECT_VERSION;
|
||||||
|
public const string SHARE_DIR;
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// gui.vala: activity tracker - GUI part
|
||||||
|
//
|
||||||
|
// Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
public class WdmtgWindow : Gtk.Window {
|
||||||
|
private unowned Sqlite.Database db;
|
||||||
|
|
||||||
|
public WdmtgWindow.with_db (Sqlite.Database db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
}
|
21
meson.build
21
meson.build
|
@ -7,8 +7,8 @@ conf = configuration_data()
|
||||||
conf.set('CMAKE_PROJECT_NAME', meson.project_name())
|
conf.set('CMAKE_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.vala.in',
|
input : 'config.h.in',
|
||||||
output : 'config.vala',
|
output : 'config.h',
|
||||||
configuration : conf,
|
configuration : conf,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,18 +20,13 @@ dependencies = [
|
||||||
dependency('gee-0.8'),
|
dependency('gee-0.8'),
|
||||||
dependency('sqlite3'),
|
dependency('sqlite3'),
|
||||||
dependency('x11'),
|
dependency('x11'),
|
||||||
|
|
||||||
# Only because of flock
|
|
||||||
meson.get_compiler('vala').find_library('posix'),
|
|
||||||
|
|
||||||
# Ours
|
|
||||||
dependency('xext'),
|
dependency('xext'),
|
||||||
dependency('xextproto'),
|
dependency('xextproto'),
|
||||||
]
|
]
|
||||||
sources = files(
|
gui = static_library('gui', 'gui.vala', 'config.vapi',
|
||||||
'wdmtg.vala',
|
install : false,
|
||||||
meson.current_build_dir() / 'config.vala',
|
|
||||||
)
|
|
||||||
executable('wdmtg', sources,
|
|
||||||
install : true,
|
|
||||||
dependencies : dependencies)
|
dependencies : dependencies)
|
||||||
|
executable('wdmtg', 'wdmtg.c',
|
||||||
|
install : true,
|
||||||
|
link_with : [gui],
|
||||||
|
dependencies : [dependencies])
|
||||||
|
|
|
@ -0,0 +1,506 @@
|
||||||
|
//
|
||||||
|
// wdmtg.c: activity tracker
|
||||||
|
//
|
||||||
|
// Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// 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>
|
||||||
|
#include <locale.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xutil.h>
|
||||||
|
#include <X11/keysym.h>
|
||||||
|
#include <X11/extensions/sync.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "gui.h"
|
||||||
|
|
||||||
|
// --- Utilities ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void
|
||||||
|
exit_fatal(const gchar *format, ...) G_GNUC_PRINTF(1, 2);
|
||||||
|
|
||||||
|
static void
|
||||||
|
exit_fatal(const gchar *format, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, format);
|
||||||
|
|
||||||
|
gchar *format_nl = g_strdup_printf("%s\n", format);
|
||||||
|
vfprintf(stderr, format_nl, ap);
|
||||||
|
free(format_nl);
|
||||||
|
|
||||||
|
va_end(ap);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GStrv
|
||||||
|
get_xdg_config_dirs(void)
|
||||||
|
{
|
||||||
|
GPtrArray *paths = g_ptr_array_new();
|
||||||
|
g_ptr_array_add(paths, g_strdup(g_get_user_config_dir()));
|
||||||
|
for (const gchar *const *sys = g_get_system_config_dirs(); *sys; sys++)
|
||||||
|
g_ptr_array_add(paths, g_strdup(*sys));
|
||||||
|
g_ptr_array_add(paths, NULL);
|
||||||
|
return (GStrv) g_ptr_array_free(paths, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Globals -----------------------------------------------------------------
|
||||||
|
|
||||||
|
struct event {
|
||||||
|
gint64 timestamp; // When the event happened
|
||||||
|
gchar *title; // Current title at the time
|
||||||
|
gboolean idle; // Whether the user is idle
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
event_free(struct event *self)
|
||||||
|
{
|
||||||
|
g_free(self->title);
|
||||||
|
g_slice_free(struct event, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
struct {
|
||||||
|
GAsyncQueue *queue; // Async queue of `struct event`
|
||||||
|
} g;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
Display *dpy; // X display handle
|
||||||
|
|
||||||
|
Atom net_active_window; // _NET_ACTIVE_WINDOW
|
||||||
|
Atom net_wm_name; // _NET_WM_NAME
|
||||||
|
|
||||||
|
// Window title tracking
|
||||||
|
|
||||||
|
gchar *current_title; // Current window title or NULL
|
||||||
|
Window current_window; // Current window
|
||||||
|
gboolean current_idle; // Current idle status
|
||||||
|
|
||||||
|
// XSync activity tracking
|
||||||
|
|
||||||
|
int xsync_base_event_code; // XSync base event code
|
||||||
|
XSyncCounter idle_counter; // XSync IDLETIME counter
|
||||||
|
XSyncValue idle_timeout; // Idle timeout
|
||||||
|
|
||||||
|
XSyncAlarm idle_alarm_inactive; // User is inactive
|
||||||
|
XSyncAlarm idle_alarm_active; // User is active
|
||||||
|
} gen;
|
||||||
|
|
||||||
|
// --- X helpers ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static XSyncCounter
|
||||||
|
get_counter(const char *name)
|
||||||
|
{
|
||||||
|
int n;
|
||||||
|
XSyncSystemCounter *counters = XSyncListSystemCounters(gen.dpy, &n);
|
||||||
|
XSyncCounter counter = None;
|
||||||
|
while (n--) {
|
||||||
|
if (!strcmp(counters[n].name, name))
|
||||||
|
counter = counters[n].counter;
|
||||||
|
}
|
||||||
|
XSyncFreeSystemCounterList(counters);
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *
|
||||||
|
x_text_property_to_utf8(XTextProperty *prop)
|
||||||
|
{
|
||||||
|
Atom utf8_string = XInternAtom(gen.dpy, "UTF8_STRING", true);
|
||||||
|
if (prop->encoding == utf8_string)
|
||||||
|
return g_strdup((char *) prop->value);
|
||||||
|
|
||||||
|
int n = 0;
|
||||||
|
char **list = NULL;
|
||||||
|
if (XmbTextPropertyToTextList(gen.dpy, prop, &list, &n) >= Success
|
||||||
|
&& n > 0 && *list) {
|
||||||
|
char *result = g_locale_to_utf8(*list, -1, NULL, NULL, NULL);
|
||||||
|
XFreeStringList(list);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *
|
||||||
|
x_text_property(Window window, Atom atom)
|
||||||
|
{
|
||||||
|
XTextProperty name;
|
||||||
|
XGetTextProperty(gen.dpy, window, &name, atom);
|
||||||
|
if (!name.value)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
char *result = x_text_property_to_utf8(&name);
|
||||||
|
XFree(name.value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- X error handling --------------------------------------------------------
|
||||||
|
|
||||||
|
static XErrorHandler g_default_x_error_handler;
|
||||||
|
|
||||||
|
static int
|
||||||
|
on_x_error(Display *dpy, XErrorEvent *ee)
|
||||||
|
{
|
||||||
|
// This just is going to happen since those windows aren't ours
|
||||||
|
if (ee->error_code == BadWindow)
|
||||||
|
return 0;
|
||||||
|
return g_default_x_error_handler(dpy, ee);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Application -------------------------------------------------------------
|
||||||
|
|
||||||
|
static char *
|
||||||
|
x_window_title(Window window)
|
||||||
|
{
|
||||||
|
char *title;
|
||||||
|
if (!(title = x_text_property(window, gen.net_wm_name))
|
||||||
|
&& !(title = x_text_property(window, XA_WM_NAME)))
|
||||||
|
title = g_strdup("broken");
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
update_window_title(char *new_title)
|
||||||
|
{
|
||||||
|
bool changed = !gen.current_title != !new_title
|
||||||
|
|| (new_title && strcmp(gen.current_title, new_title));
|
||||||
|
free(gen.current_title);
|
||||||
|
gen.current_title = new_title;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
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->idle = gen.current_idle;
|
||||||
|
g_async_queue_push(g.queue, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
update_current_window(void)
|
||||||
|
{
|
||||||
|
Window root = DefaultRootWindow(gen.dpy);
|
||||||
|
|
||||||
|
Atom dummy_type; int dummy_format; unsigned long nitems, dummy_bytes;
|
||||||
|
unsigned char *p = NULL;
|
||||||
|
if (XGetWindowProperty(gen.dpy, root, gen.net_active_window,
|
||||||
|
0L, 1L, false, XA_WINDOW, &dummy_type, &dummy_format,
|
||||||
|
&nitems, &dummy_bytes, &p) != Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char *new_title = NULL;
|
||||||
|
if (nitems) {
|
||||||
|
Window active_window = *(Window *) p;
|
||||||
|
XFree(p);
|
||||||
|
|
||||||
|
if (gen.current_window != active_window && gen.current_window)
|
||||||
|
XSelectInput(gen.dpy, gen.current_window, 0);
|
||||||
|
|
||||||
|
XSelectInput(gen.dpy, active_window, PropertyChangeMask);
|
||||||
|
new_title = x_window_title(active_window);
|
||||||
|
gen.current_window = active_window;
|
||||||
|
}
|
||||||
|
if (update_window_title(new_title)) {
|
||||||
|
printf("Window changed: %s\n",
|
||||||
|
gen.current_title ? gen.current_title : "(none)");
|
||||||
|
push_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_x_property_notify(XPropertyEvent *ev)
|
||||||
|
{
|
||||||
|
// This is from the EWMH specification, set by the window manager
|
||||||
|
if (ev->atom == gen.net_active_window) {
|
||||||
|
update_current_window();
|
||||||
|
} else if (ev->window == gen.current_window &&
|
||||||
|
ev->atom == gen.net_wm_name) {
|
||||||
|
if (update_window_title(x_window_title(ev->window))) {
|
||||||
|
printf("Title changed: %s\n", gen.current_title);
|
||||||
|
push_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
set_idle_alarm(XSyncAlarm *alarm, XSyncTestType test, XSyncValue value)
|
||||||
|
{
|
||||||
|
XSyncAlarmAttributes attr;
|
||||||
|
attr.trigger.counter = gen.idle_counter;
|
||||||
|
attr.trigger.test_type = test;
|
||||||
|
attr.trigger.wait_value = value;
|
||||||
|
XSyncIntToValue(&attr.delta, 0);
|
||||||
|
|
||||||
|
long flags = XSyncCACounter | XSyncCATestType | XSyncCAValue | XSyncCADelta;
|
||||||
|
if (*alarm)
|
||||||
|
XSyncChangeAlarm(gen.dpy, *alarm, flags, &attr);
|
||||||
|
else
|
||||||
|
*alarm = XSyncCreateAlarm(gen.dpy, flags, &attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
on_x_alarm_notify(XSyncAlarmNotifyEvent *ev)
|
||||||
|
{
|
||||||
|
if (ev->alarm == gen.idle_alarm_inactive) {
|
||||||
|
printf("User is inactive\n");
|
||||||
|
gen.current_idle = true;
|
||||||
|
push_event();
|
||||||
|
|
||||||
|
XSyncValue one, minus_one;
|
||||||
|
XSyncIntToValue(&one, 1);
|
||||||
|
|
||||||
|
Bool overflow;
|
||||||
|
XSyncValueSubtract(&minus_one, ev->counter_value, one, &overflow);
|
||||||
|
|
||||||
|
// Set an alarm for IDLETIME <= current_idletime - 1
|
||||||
|
set_idle_alarm(&gen.idle_alarm_active,
|
||||||
|
XSyncNegativeComparison, minus_one);
|
||||||
|
} else if (ev->alarm == gen.idle_alarm_active) {
|
||||||
|
printf("User is active\n");
|
||||||
|
gen.current_idle = false;
|
||||||
|
push_event();
|
||||||
|
|
||||||
|
set_idle_alarm(&gen.idle_alarm_inactive,
|
||||||
|
XSyncPositiveComparison, gen.idle_timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
on_x_ready(G_GNUC_UNUSED gpointer user_data)
|
||||||
|
{
|
||||||
|
XEvent ev;
|
||||||
|
while (XPending(gen.dpy)) {
|
||||||
|
if (XNextEvent(gen.dpy, &ev))
|
||||||
|
exit_fatal("XNextEvent returned non-zero");
|
||||||
|
else if (ev.type == PropertyNotify)
|
||||||
|
on_x_property_notify(&ev.xproperty);
|
||||||
|
else if (ev.type == gen.xsync_base_event_code + XSyncAlarmNotify)
|
||||||
|
on_x_alarm_notify((XSyncAlarmNotifyEvent *) &ev);
|
||||||
|
}
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
generate_events(void)
|
||||||
|
{
|
||||||
|
GIOChannel *channel = g_io_channel_unix_new(ConnectionNumber(gen.dpy));
|
||||||
|
GSource *watch = g_io_create_watch(channel, G_IO_IN);
|
||||||
|
g_source_set_callback(watch, on_x_ready, NULL, NULL);
|
||||||
|
|
||||||
|
GMainLoop *loop =
|
||||||
|
g_main_loop_new(g_main_context_get_thread_default(), false);
|
||||||
|
g_source_attach(watch, g_main_loop_get_context(loop));
|
||||||
|
g_main_loop_run(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
gboolean show_version = false;
|
||||||
|
const GOptionEntry options[] = {
|
||||||
|
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
|
||||||
|
&show_version, "output version information and exit", NULL},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
|
||||||
|
GError *error = NULL;
|
||||||
|
if (!gtk_init_with_args(&argc, &argv, " - activity tracker",
|
||||||
|
options, NULL, &error))
|
||||||
|
exit_fatal("%s", error->message);
|
||||||
|
if (show_version) {
|
||||||
|
printf(PROJECT_NAME " " PROJECT_VERSION "\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.queue = g_async_queue_new_full((GDestroyNotify) event_free);
|
||||||
|
|
||||||
|
if (!setlocale(LC_CTYPE, ""))
|
||||||
|
exit_fatal("cannot set locale");
|
||||||
|
if (!XSupportsLocale())
|
||||||
|
exit_fatal("locale not supported by Xlib");
|
||||||
|
|
||||||
|
XInitThreads();
|
||||||
|
if (!(gen.dpy = XOpenDisplay(NULL)))
|
||||||
|
exit_fatal("cannot open display");
|
||||||
|
|
||||||
|
gen.net_active_window = XInternAtom(gen.dpy, "_NET_ACTIVE_WINDOW", true);
|
||||||
|
gen.net_wm_name = XInternAtom(gen.dpy, "_NET_WM_NAME", true);
|
||||||
|
|
||||||
|
// TODO: it is possible to employ a fallback mechanism via XScreenSaver
|
||||||
|
// by polling the XScreenSaverInfo::idle field, see
|
||||||
|
// https://www.x.org/releases/X11R7.5/doc/man/man3/Xss.3.html
|
||||||
|
|
||||||
|
int dummy;
|
||||||
|
if (!XSyncQueryExtension(gen.dpy, &gen.xsync_base_event_code, &dummy)
|
||||||
|
|| !XSyncInitialize(gen.dpy, &dummy, &dummy))
|
||||||
|
exit_fatal("cannot initialize XSync");
|
||||||
|
|
||||||
|
// The idle counter is not guaranteed to exist, only SERVERTIME is
|
||||||
|
if (!(gen.idle_counter = get_counter("IDLETIME")))
|
||||||
|
exit_fatal("idle counter is missing");
|
||||||
|
|
||||||
|
Window root = DefaultRootWindow(gen.dpy);
|
||||||
|
XSelectInput(gen.dpy, root, PropertyChangeMask);
|
||||||
|
XSync(gen.dpy, False);
|
||||||
|
// TODO: what is the interaction with GTK+ here?
|
||||||
|
g_default_x_error_handler = XSetErrorHandler(on_x_error);
|
||||||
|
|
||||||
|
GKeyFile *kf = g_key_file_new();
|
||||||
|
gchar *subpath = g_build_filename(PROJECT_NAME, PROJECT_NAME ".conf", NULL);
|
||||||
|
GStrv dirs = get_xdg_config_dirs();
|
||||||
|
|
||||||
|
int timeout = 600;
|
||||||
|
if (g_key_file_load_from_dirs(kf,
|
||||||
|
subpath, (const gchar **) dirs, NULL, 0, NULL)) {
|
||||||
|
guint64 n = g_key_file_get_uint64(kf, "Settings", "idle_timeout", NULL);
|
||||||
|
if (n > 0 && n <= G_MAXINT / 1000)
|
||||||
|
timeout = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_strfreev(dirs);
|
||||||
|
g_free(subpath);
|
||||||
|
g_key_file_free(kf);
|
||||||
|
|
||||||
|
XSyncIntToValue(&gen.idle_timeout, timeout * 1000);
|
||||||
|
update_current_window();
|
||||||
|
set_idle_alarm(&gen.idle_alarm_inactive,
|
||||||
|
XSyncPositiveComparison, gen.idle_timeout);
|
||||||
|
|
||||||
|
gchar *data_path =
|
||||||
|
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
|
||||||
|
g_mkdir_with_parents(data_path, 0755);
|
||||||
|
|
||||||
|
// TODO: try exclusivity/invocation either via DBus directly,
|
||||||
|
// or via GApplication or GtkApplication:
|
||||||
|
// - GtkApplication calls Gtk.init automatically during "startup" signal,
|
||||||
|
// Gtk.init doesn't get command line args
|
||||||
|
// - "inhibiting" makes no sense, it can't be used for mere delays
|
||||||
|
// - actually, the "query-end" signal
|
||||||
|
// - should check whether it tries to exit cleanly
|
||||||
|
// - what is the session manager, do I have it?
|
||||||
|
// - "register-session" looks useful
|
||||||
|
// - GTK+ keeps the application running as long as it has windows,
|
||||||
|
// though I want to keep it running forever
|
||||||
|
// - g_application_hold(), perhaps
|
||||||
|
// - so maybe just use GApplication, that will provide more control
|
||||||
|
|
||||||
|
// Bind to a control socket, also ensuring only one instance is running
|
||||||
|
gchar *socket_path = g_build_filename(data_path, "socket", NULL);
|
||||||
|
|
||||||
|
struct flock lock =
|
||||||
|
{
|
||||||
|
.l_type = F_WRLCK,
|
||||||
|
.l_start = 0,
|
||||||
|
.l_whence = SEEK_SET,
|
||||||
|
.l_len = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
gchar *lock_path = g_strdup_printf("%s.lock", socket_path);
|
||||||
|
int lock_fd = open(lock_path, O_RDWR | O_CREAT, 0644);
|
||||||
|
if (fcntl(lock_fd, F_SETLK, &lock))
|
||||||
|
exit_fatal("failed to acquire lock: %s", strerror(errno));
|
||||||
|
unlink(socket_path);
|
||||||
|
|
||||||
|
int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
|
if (socket_fd < 0)
|
||||||
|
exit_fatal("%s: %s", socket_path, strerror(errno));
|
||||||
|
|
||||||
|
struct sockaddr_un sun;
|
||||||
|
sun.sun_family = AF_UNIX;
|
||||||
|
strncpy(sun.sun_path, socket_path, sizeof sun.sun_path);
|
||||||
|
if (bind(socket_fd, (struct sockaddr *) &sun, sizeof sun))
|
||||||
|
exit_fatal("%s: %s", socket_path, strerror(errno));
|
||||||
|
if (listen(socket_fd, 10))
|
||||||
|
exit_fatal("%s: %s", socket_path, strerror(errno));
|
||||||
|
|
||||||
|
sqlite3 *db = NULL;
|
||||||
|
gchar *db_path = g_build_filename(data_path, "db.sqlite", NULL);
|
||||||
|
int rc = sqlite3_open(db_path, &db);
|
||||||
|
if (rc != SQLITE_OK)
|
||||||
|
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
|
||||||
|
|
||||||
|
// This shouldn't normally happen but external applications may decide
|
||||||
|
// to read things out, and mess with us. When that takes too long, we may
|
||||||
|
// a/ wait for it to finish, b/ start with a whole-database lock or, even
|
||||||
|
// more simply, c/ crash on the BUSY error.
|
||||||
|
sqlite3_busy_timeout(db, 1000);
|
||||||
|
|
||||||
|
char *errmsg = NULL;
|
||||||
|
if ((rc = sqlite3_exec(db, "BEGIN", NULL, NULL, &errmsg)))
|
||||||
|
exit_fatal("%s: %s", db_path, errmsg);
|
||||||
|
|
||||||
|
sqlite3_stmt *stmt = NULL;
|
||||||
|
if ((rc = sqlite3_prepare_v2(db, "PRAGMA user_version", -1, &stmt, NULL)))
|
||||||
|
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
|
||||||
|
if ((rc = sqlite3_step(stmt)) != SQLITE_ROW ||
|
||||||
|
sqlite3_data_count(stmt) != 1)
|
||||||
|
exit_fatal("%s: %s", db_path, "cannot retrieve user version");
|
||||||
|
|
||||||
|
int user_version = sqlite3_column_int(stmt, 0);
|
||||||
|
if (user_version == 0) {
|
||||||
|
if ((rc = sqlite3_exec(db, "CREATE TABLE events ("
|
||||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||||
|
"timestamp INTEGER, "
|
||||||
|
"title TEXT, "
|
||||||
|
"idle BOOLEAN)", NULL, NULL, &errmsg)))
|
||||||
|
exit_fatal("%s: %s", db_path, errmsg);
|
||||||
|
if ((rc = sqlite3_exec(db, "PRAGMA user_version = 1", NULL, NULL,
|
||||||
|
&errmsg)))
|
||||||
|
exit_fatal("%s: %s", db_path, errmsg);
|
||||||
|
} else if (user_version != 1) {
|
||||||
|
exit_fatal("%s: unsupported DB version: %d", db_path, user_version);
|
||||||
|
}
|
||||||
|
if ((rc = sqlite3_exec(db, "COMMIT", NULL, NULL, &errmsg)))
|
||||||
|
exit_fatal("%s: %s", db_path, errmsg);
|
||||||
|
|
||||||
|
g_free(db_path);
|
||||||
|
g_free(lock_path);
|
||||||
|
g_free(socket_path);
|
||||||
|
g_free(data_path);
|
||||||
|
|
||||||
|
GThread *generator =
|
||||||
|
g_thread_new("generator", (GThreadFunc) generate_events, NULL);
|
||||||
|
|
||||||
|
// TODO: somehow read events from the async queue
|
||||||
|
// TODO: how in the name of fuck would our custom source wake up a sleeping
|
||||||
|
// main loop? There is g_main_context_wakeup() but...
|
||||||
|
// - GWakeUp is internal, apparently
|
||||||
|
|
||||||
|
// TODO: listen for connections on the control socket
|
||||||
|
|
||||||
|
WdmtgWindow *window = wdmtg_window_new_with_db(db);
|
||||||
|
|
||||||
|
gtk_main();
|
||||||
|
g_thread_join(generator);
|
||||||
|
|
||||||
|
free(gen.current_title);
|
||||||
|
XCloseDisplay(gen.dpy);
|
||||||
|
return 0;
|
||||||
|
}
|
406
wdmtg.vala
406
wdmtg.vala
|
@ -1,406 +0,0 @@
|
||||||
//
|
|
||||||
// wdmtg.vala: activity tracker
|
|
||||||
//
|
|
||||||
// Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
//
|
|
||||||
// vim: set sw=2 ts=2 sts=2 et tw=80:
|
|
||||||
// modules: x11 xext config
|
|
||||||
// vapidirs: . ../build build
|
|
||||||
|
|
||||||
namespace Wdmtg {
|
|
||||||
|
|
||||||
// --- Utilities ---------------------------------------------------------------
|
|
||||||
|
|
||||||
void exit_fatal (string format, ...) {
|
|
||||||
stderr.vprintf ("fatal: " + format + "\n", va_list ());
|
|
||||||
Process.exit (1);
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] get_xdg_config_dirs () {
|
|
||||||
string[] paths = { Environment.get_user_config_dir () };
|
|
||||||
foreach (var system_path in Environment.get_system_config_dirs ())
|
|
||||||
paths += system_path;
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Globals -----------------------------------------------------------------
|
|
||||||
|
|
||||||
X.Display dpy; ///< X display handle
|
|
||||||
int sync_base; ///< Sync extension base
|
|
||||||
|
|
||||||
X.ID idle_counter; ///< XSync IDLETIME counter
|
|
||||||
X.Sync.Value idle_timeout; ///< User idle timeout
|
|
||||||
|
|
||||||
X.ID idle_alarm_inactive; ///< User is inactive
|
|
||||||
X.ID idle_alarm_active; ///< User is active
|
|
||||||
|
|
||||||
X.Atom net_active_window; ///< _NET_ACTIVE_WINDOW atom
|
|
||||||
X.Atom net_wm_name; ///< _NET_WM_NAME atom
|
|
||||||
|
|
||||||
string? current_title; ///< Current window title
|
|
||||||
X.Window current_window; ///< Current window
|
|
||||||
bool current_idle = false; ///< Current idle status
|
|
||||||
|
|
||||||
struct Event {
|
|
||||||
public int64 timestamp; ///< When the event happened
|
|
||||||
public string? title; ///< Current title at the time
|
|
||||||
public bool idle; ///< Whether the user is idle
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncQueue<Event?> queue; ///< Async queue
|
|
||||||
|
|
||||||
// --- X helpers ---------------------------------------------------------------
|
|
||||||
|
|
||||||
X.ID get_counter (string name) {
|
|
||||||
int n_counters = 0;
|
|
||||||
var counters = X.Sync.list_system_counters (dpy, out n_counters);
|
|
||||||
X.ID counter = X.None;
|
|
||||||
while (n_counters-- > 0) {
|
|
||||||
if (counters[n_counters].name == name)
|
|
||||||
counter = counters[n_counters].counter;
|
|
||||||
}
|
|
||||||
X.Sync.free_system_counter_list (counters);
|
|
||||||
return counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? x_text_property_to_utf8 (ref X.TextProperty prop) {
|
|
||||||
X.Atom utf8_string = dpy.intern_atom ("UTF8_STRING", true);
|
|
||||||
if (prop.encoding == utf8_string)
|
|
||||||
return (string) prop.value;
|
|
||||||
|
|
||||||
int n = 0;
|
|
||||||
uint8 **list = null;
|
|
||||||
if (X.mb_text_property_to_text_list (dpy, ref prop, out list, out n)
|
|
||||||
>= X.Success && n > 0 && null != list[0]) {
|
|
||||||
var result = ((string) list[0]).locale_to_utf8 (-1, null, null);
|
|
||||||
X.free_string_list (list);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? x_text_property (X.Window window, X.Atom atom) {
|
|
||||||
X.TextProperty name;
|
|
||||||
X.get_text_property (dpy, window, out name, atom);
|
|
||||||
if (null == name.@value)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
string? result = x_text_property_to_utf8 (ref name);
|
|
||||||
X.free (name.@value);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- X error handling --------------------------------------------------------
|
|
||||||
|
|
||||||
X.ErrorHandler default_x_error_handler;
|
|
||||||
|
|
||||||
int on_x_error (X.Display dpy, X.ErrorEvent *ee) {
|
|
||||||
// This just is going to happen since those windows aren't ours
|
|
||||||
if (ee.error_code == X.ErrorCode.BAD_WINDOW)
|
|
||||||
return 0;
|
|
||||||
return default_x_error_handler (dpy, ee);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Application -------------------------------------------------------------
|
|
||||||
|
|
||||||
string x_window_title (X.Window window) {
|
|
||||||
string? title;
|
|
||||||
if (null == (title = x_text_property (window, net_wm_name))
|
|
||||||
&& null == (title = x_text_property (window, X.XA_WM_NAME)))
|
|
||||||
title = "broken";
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool update_window_title (string? new_title) {
|
|
||||||
bool changed = (null == current_title) != (null == new_title)
|
|
||||||
|| current_title != new_title;
|
|
||||||
current_title = new_title;
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void push_event () {
|
|
||||||
queue.push(Event () {
|
|
||||||
timestamp = get_real_time (),
|
|
||||||
title = current_title,
|
|
||||||
idle = current_idle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void update_current_window () {
|
|
||||||
var root = dpy.default_root_window ();
|
|
||||||
X.Atom dummy_type; int dummy_format; ulong nitems, dummy_bytes;
|
|
||||||
void *p = null;
|
|
||||||
if (dpy.get_window_property (root, net_active_window,
|
|
||||||
0, 1, false, X.XA_WINDOW, out dummy_type, out dummy_format,
|
|
||||||
out nitems, out dummy_bytes, out p) != X.Success)
|
|
||||||
return;
|
|
||||||
|
|
||||||
string? new_title = null;
|
|
||||||
if (0 != nitems) {
|
|
||||||
X.Window active_window = *(X.Window *) p;
|
|
||||||
X.free (p);
|
|
||||||
|
|
||||||
if (current_window != active_window && X.None != current_window)
|
|
||||||
dpy.select_input (current_window, 0);
|
|
||||||
dpy.select_input (active_window, X.EventMask.PropertyChangeMask);
|
|
||||||
new_title = x_window_title (active_window);
|
|
||||||
current_window = active_window;
|
|
||||||
}
|
|
||||||
if (update_window_title (new_title)) {
|
|
||||||
stdout.printf ("Window changed: %s\n",
|
|
||||||
null != current_title ? current_title : "(none)");
|
|
||||||
push_event ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_x_property_notify (X.PropertyEvent *xproperty) {
|
|
||||||
// This is from the EWMH specification, set by the window manager
|
|
||||||
if (xproperty.atom == net_active_window)
|
|
||||||
update_current_window ();
|
|
||||||
else if (xproperty.window == current_window
|
|
||||||
&& xproperty.atom == net_wm_name) {
|
|
||||||
if (update_window_title (x_window_title (current_window))) {
|
|
||||||
stdout.printf ("Title changed: %s\n", current_title);
|
|
||||||
push_event ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_idle_alarm
|
|
||||||
(ref X.ID alarm, X.Sync.TestType test, X.Sync.Value @value) {
|
|
||||||
X.Sync.AlarmAttributes attr = {};
|
|
||||||
attr.trigger.counter = idle_counter;
|
|
||||||
attr.trigger.test_type = test;
|
|
||||||
attr.trigger.wait_value = @value;
|
|
||||||
X.Sync.int_to_value (out attr.delta, 0);
|
|
||||||
|
|
||||||
X.Sync.CA flags = X.Sync.CA.Counter | X.Sync.CA.TestType
|
|
||||||
| X.Sync.CA.Value | X.Sync.CA.Delta;
|
|
||||||
if (X.None != alarm)
|
|
||||||
X.Sync.change_alarm (dpy, alarm, flags, ref attr);
|
|
||||||
else
|
|
||||||
alarm = X.Sync.create_alarm (dpy, flags, ref attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void on_x_alarm_notify (X.Sync.AlarmNotifyEvent *xalarm) {
|
|
||||||
if (xalarm.alarm == idle_alarm_inactive) {
|
|
||||||
stdout.printf ("User is inactive\n");
|
|
||||||
current_idle = true;
|
|
||||||
push_event ();
|
|
||||||
|
|
||||||
X.Sync.Value one, minus_one;
|
|
||||||
X.Sync.int_to_value (out one, 1);
|
|
||||||
|
|
||||||
int overflow;
|
|
||||||
X.Sync.value_subtract
|
|
||||||
(out minus_one, xalarm.counter_value, one, out overflow);
|
|
||||||
|
|
||||||
// Set an alarm for IDLETIME <= current_idletime - 1
|
|
||||||
set_idle_alarm (ref idle_alarm_active,
|
|
||||||
X.Sync.TestType.NegativeComparison, minus_one);
|
|
||||||
} else if (xalarm.alarm == idle_alarm_inactive) {
|
|
||||||
stdout.printf ("User is active\n");
|
|
||||||
current_idle = false;
|
|
||||||
push_event ();
|
|
||||||
|
|
||||||
set_idle_alarm (ref idle_alarm_inactive,
|
|
||||||
X.Sync.TestType.PositiveComparison, idle_timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void generate_events () {
|
|
||||||
var channel = new IOChannel.unix_new (dpy.connection_number ());
|
|
||||||
var watch = channel.create_watch (IOCondition.IN);
|
|
||||||
watch.set_callback (() => {
|
|
||||||
X.Event ev = {0};
|
|
||||||
while (0 != dpy.pending ()) {
|
|
||||||
if (0 != dpy.next_event (ref ev)) {
|
|
||||||
exit_fatal ("XNextEvent returned non-zero");
|
|
||||||
} else if (ev.type == X.EventType.PropertyNotify) {
|
|
||||||
on_x_property_notify (&ev.xproperty);
|
|
||||||
} else if (ev.type == sync_base + X.Sync.EventType.AlarmNotify) {
|
|
||||||
on_x_alarm_notify ((X.Sync.AlarmNotifyEvent *) (&ev));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
var loop = new MainLoop (MainContext.get_thread_default ());
|
|
||||||
watch.attach (loop.get_context ());
|
|
||||||
loop.run ();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool show_version;
|
|
||||||
const OptionEntry[] options = {
|
|
||||||
{ "version", 'V', OptionFlags.IN_MAIN, OptionArg.NONE, ref show_version,
|
|
||||||
"output version information and exit" },
|
|
||||||
{ null }
|
|
||||||
};
|
|
||||||
|
|
||||||
public int main (string[] args) {
|
|
||||||
if (null == Intl.setlocale (GLib.LocaleCategory.CTYPE))
|
|
||||||
exit_fatal ("cannot set locale");
|
|
||||||
if (0 == X.supports_locale ())
|
|
||||||
exit_fatal ("locale not supported by Xlib");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Gtk.init_with_args (ref args, " - activity tracker", options, null);
|
|
||||||
} catch (OptionError e) {
|
|
||||||
exit_fatal ("option parsing failed: %s", e.message);
|
|
||||||
} catch (Error e) {
|
|
||||||
exit_fatal ("%s", e.message);
|
|
||||||
}
|
|
||||||
if (show_version) {
|
|
||||||
stdout.printf (Config.PROJECT_NAME + " " + Config.PROJECT_VERSION + "\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
queue = new AsyncQueue<Event?> ();
|
|
||||||
|
|
||||||
X.init_threads ();
|
|
||||||
if (null == (dpy = new X.Display ()))
|
|
||||||
exit_fatal ("cannot open display");
|
|
||||||
|
|
||||||
net_active_window = dpy.intern_atom ("_NET_ACTIVE_WINDOW", true);
|
|
||||||
net_wm_name = dpy.intern_atom ("_NET_WM_NAME", true);
|
|
||||||
|
|
||||||
// TODO: it is possible to employ a fallback mechanism via XScreenSaver
|
|
||||||
// by polling the XScreenSaverInfo::idle field, see
|
|
||||||
// https://www.x.org/releases/X11R7.5/doc/man/man3/Xss.3.html
|
|
||||||
|
|
||||||
int dummy;
|
|
||||||
if (0 == X.Sync.query_extension (dpy, out sync_base, out dummy)
|
|
||||||
|| 0 == X.Sync.initialize (dpy, out dummy, out dummy))
|
|
||||||
exit_fatal ("cannot initialize XSync");
|
|
||||||
|
|
||||||
// The idle counter is not guaranteed to exist, only SERVERTIME is
|
|
||||||
if (X.None == (idle_counter = get_counter ("IDLETIME")))
|
|
||||||
exit_fatal ("idle counter is missing");
|
|
||||||
|
|
||||||
var root = dpy.default_root_window ();
|
|
||||||
dpy.select_input (root, X.EventMask.PropertyChangeMask);
|
|
||||||
X.sync (dpy, false);
|
|
||||||
default_x_error_handler = X.set_error_handler (on_x_error);
|
|
||||||
|
|
||||||
int timeout = 600; // 10 minutes by default
|
|
||||||
try {
|
|
||||||
var kf = new KeyFile ();
|
|
||||||
kf.load_from_dirs (Config.PROJECT_NAME + Path.DIR_SEPARATOR_S
|
|
||||||
+ Config.PROJECT_NAME + ".conf", get_xdg_config_dirs (), null, 0);
|
|
||||||
|
|
||||||
var n = kf.get_uint64 ("Settings", "idle_timeout");
|
|
||||||
if (0 != n && n <= int.MAX / 1000)
|
|
||||||
timeout = (int) n;
|
|
||||||
} catch (Error e) {
|
|
||||||
// Ignore errors this far, keeping the defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
X.Sync.int_to_value (out idle_timeout, timeout * 1000);
|
|
||||||
update_current_window ();
|
|
||||||
set_idle_alarm (ref idle_alarm_inactive,
|
|
||||||
X.Sync.TestType.PositiveComparison, idle_timeout);
|
|
||||||
|
|
||||||
var data_path = Path.build_filename (Environment.get_user_data_dir (),
|
|
||||||
Config.PROJECT_NAME);
|
|
||||||
DirUtils.create_with_parents (data_path, 0755);
|
|
||||||
|
|
||||||
// TODO: try exclusivity/invocation either via DBus directly,
|
|
||||||
// or via GApplication or GtkApplication:
|
|
||||||
// - GtkApplication calls Gtk.init automatically during "startup" signal,
|
|
||||||
// Gtk.init doesn't get command line args
|
|
||||||
// - "inhibiting" makes no sense, it can't be used for mere delays
|
|
||||||
// - actually, the "query-end" signal
|
|
||||||
// - should check whether it tries to exit cleanly
|
|
||||||
// - what is the session manager, do I have it?
|
|
||||||
// - "register-session" looks useful
|
|
||||||
// - GTK+ keeps the application running as long as it has windows,
|
|
||||||
// though I want to keep it running forever
|
|
||||||
// - g_application_hold(), perhaps
|
|
||||||
// - so maybe just use GApplication, that will provide more control
|
|
||||||
|
|
||||||
// Bind to a control socket, also ensuring only one instance is running
|
|
||||||
var socket_path = Path.build_filename (data_path, "socket");
|
|
||||||
|
|
||||||
Posix.Flock fl = Posix.Flock () {
|
|
||||||
l_type = Posix.F_WRLCK,
|
|
||||||
l_start = 0,
|
|
||||||
l_whence = Posix.SEEK_SET,
|
|
||||||
l_len = 0
|
|
||||||
};
|
|
||||||
|
|
||||||
var lk = FileStream.open (socket_path + ".lock", "w");
|
|
||||||
if (Posix.fcntl (lk.fileno (), Posix.F_SETLK, &fl) < 0)
|
|
||||||
exit_fatal("failed to acquire lock: %s", Posix.errno.to_string ());
|
|
||||||
FileUtils.unlink (socket_path);
|
|
||||||
|
|
||||||
Socket socket;
|
|
||||||
try {
|
|
||||||
socket = new Socket (SocketFamily.UNIX, SocketType.STREAM,
|
|
||||||
SocketProtocol.DEFAULT);
|
|
||||||
socket.bind (new UnixSocketAddress (socket_path), true /* allow_reuse */);
|
|
||||||
socket.listen ();
|
|
||||||
} catch (Error e) {
|
|
||||||
exit_fatal ("%s: %s", socket_path, e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Sqlite.Database db;
|
|
||||||
var db_path = Path.build_filename (data_path, "db.sqlite");
|
|
||||||
int rc = Sqlite.Database.open (db_path, out db);
|
|
||||||
if (rc != Sqlite.OK)
|
|
||||||
exit_fatal ("%s: %s", db_path, db.errmsg ());
|
|
||||||
|
|
||||||
// This shouldn't normally happen but external applications may decide
|
|
||||||
// to read things out, and mess with us. When that takes too long, we may
|
|
||||||
// a/ wait for it to finish, b/ start with a whole-database lock or, even
|
|
||||||
// more simply, c/ crash on the BUSY error.
|
|
||||||
db.busy_timeout (1000);
|
|
||||||
|
|
||||||
string errmsg;
|
|
||||||
if ((rc = db.exec ("BEGIN", null, out errmsg)) != Sqlite.OK)
|
|
||||||
exit_fatal ("%s: %s", db_path, errmsg);
|
|
||||||
|
|
||||||
Sqlite.Statement stmt;
|
|
||||||
if ((rc = db.prepare_v2 ("PRAGMA user_version", -1, out stmt, null))
|
|
||||||
!= Sqlite.OK)
|
|
||||||
exit_fatal ("%s: %s", db_path, db.errmsg ());
|
|
||||||
if ((rc = stmt.step ()) != Sqlite.ROW || stmt.data_count () != 1)
|
|
||||||
exit_fatal ("%s: %s", db_path, "cannot retrieve user version");
|
|
||||||
|
|
||||||
var user_version = stmt.column_int (0);
|
|
||||||
if (user_version == 0) {
|
|
||||||
if ((rc = db.exec ("""CREATE TABLE events (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
timestamp INTEGER,
|
|
||||||
title TEXT,
|
|
||||||
idle BOOLEAN
|
|
||||||
)""", null, out errmsg)) != Sqlite.OK)
|
|
||||||
exit_fatal ("%s: %s", db_path, errmsg);
|
|
||||||
|
|
||||||
if ((rc = db.exec ("PRAGMA user_version = 1", null, out errmsg))
|
|
||||||
!= Sqlite.OK)
|
|
||||||
exit_fatal ("%s: %s", db_path, errmsg);
|
|
||||||
} else if (user_version != 1) {
|
|
||||||
exit_fatal ("%s: unsupported DB version: %d", db_path, user_version);
|
|
||||||
}
|
|
||||||
if ((rc = db.exec ("COMMIT", null, out errmsg)) != Sqlite.OK)
|
|
||||||
exit_fatal ("%s: %s", db_path, errmsg);
|
|
||||||
|
|
||||||
var generator = new Thread<void> ("generator", generate_events);
|
|
||||||
// TODO: somehow read events from the async queue
|
|
||||||
// TODO: listen for connections on the control socket
|
|
||||||
Gtk.main ();
|
|
||||||
generator.join ();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
253
xext.vapi
253
xext.vapi
|
@ -1,252 +1 @@
|
||||||
//
|
// https://github.com/mesonbuild/meson/issues/1195
|
||||||
// xext.vapi: various extensions to the x11 vapi
|
|
||||||
//
|
|
||||||
// Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p.janouch@gmail.com>
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
//
|
|
||||||
// vim: set sw=2 ts=2 sts=2 et tw=80:
|
|
||||||
// modules: x11
|
|
||||||
|
|
||||||
namespace X {
|
|
||||||
[CCode (cname = "XErrorHandler", has_target = false)]
|
|
||||||
public delegate int ErrorHandler (Display dpy, ErrorEvent *ee);
|
|
||||||
[CCode (cname = "XSetErrorHandler")]
|
|
||||||
public ErrorHandler set_error_handler (ErrorHandler handler);
|
|
||||||
|
|
||||||
// XXX: can we extend the Display class so that the argument goes away?
|
|
||||||
[CCode (cname = "XSync")]
|
|
||||||
public int sync (Display dpy, bool discard);
|
|
||||||
[CCode (cname = "XSupportsLocale")]
|
|
||||||
public int supports_locale ();
|
|
||||||
|
|
||||||
[CCode (cname = "XTextProperty", has_type_id = false)]
|
|
||||||
public struct TextProperty {
|
|
||||||
// There is always a null byte at the end but it may also appear earlier
|
|
||||||
// depending on the other fields, so this is a bit misleading
|
|
||||||
[CCode (array_null_terminated = true)]
|
|
||||||
// Vala tries to g_free0() owned arrays, you still need to call XFree()
|
|
||||||
public unowned uint8[]? @value;
|
|
||||||
public Atom encoding;
|
|
||||||
public int format;
|
|
||||||
public ulong nitems;
|
|
||||||
}
|
|
||||||
[CCode (cname = "XGetTextProperty")]
|
|
||||||
public int get_text_property (Display dpy,
|
|
||||||
Window window, out TextProperty text_prop_return, Atom property);
|
|
||||||
|
|
||||||
[CCode (cname = "XmbTextPropertyToTextList")]
|
|
||||||
public int mb_text_property_to_text_list (Display dpy,
|
|
||||||
ref TextProperty text_prop,
|
|
||||||
[CCode (type = "char ***")] out uint8 **list_return, out int count_return);
|
|
||||||
[CCode (cname = "XFreeStringList")]
|
|
||||||
public void free_string_list ([CCode (type = "char **")] uint8** list);
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace X {
|
|
||||||
[CCode (cprefix = "", cheader_filename = "X11/extensions/sync.h")]
|
|
||||||
namespace Sync {
|
|
||||||
[CCode (cprefix = "XSync", cname = "int", has_type_id = false)]
|
|
||||||
public enum EventType {
|
|
||||||
CounterNotify,
|
|
||||||
AlarmNotify
|
|
||||||
}
|
|
||||||
|
|
||||||
[CCode (cprefix = "", cname = "int", has_type_id = false)]
|
|
||||||
public enum ErrorCode {
|
|
||||||
[CCode (cname = "XSyncBadCounter")]
|
|
||||||
BAD_COUNTER,
|
|
||||||
[CCode (cname = "XSyncBadAlarm")]
|
|
||||||
BAD_ALARM,
|
|
||||||
[CCode (cname = "XSyncBadFence")]
|
|
||||||
BAD_FENCE
|
|
||||||
}
|
|
||||||
|
|
||||||
[CCode (cprefix = "XSyncCA", cname = "int")]
|
|
||||||
[Flags]
|
|
||||||
public enum CA {
|
|
||||||
Counter,
|
|
||||||
ValueType,
|
|
||||||
Value,
|
|
||||||
TestType,
|
|
||||||
Delta,
|
|
||||||
Events
|
|
||||||
}
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncValueType", cprefix = "XSync")]
|
|
||||||
public enum ValueType {
|
|
||||||
Absolute,
|
|
||||||
Relative
|
|
||||||
}
|
|
||||||
[CCode (cname = "XSyncTestType", cprefix = "XSync")]
|
|
||||||
public enum TestType {
|
|
||||||
PositiveTransition,
|
|
||||||
NegativeTransition,
|
|
||||||
PositiveComparison,
|
|
||||||
NegativeComparison
|
|
||||||
}
|
|
||||||
[CCode (cname = "XSyncAlarmState", cprefix = "XSyncAlarm")]
|
|
||||||
public enum AlarmState {
|
|
||||||
Active,
|
|
||||||
Inactive,
|
|
||||||
Destroyed
|
|
||||||
}
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncValue", has_type_id = false)]
|
|
||||||
[SimpleType]
|
|
||||||
public struct Value {
|
|
||||||
public int hi;
|
|
||||||
public uint lo;
|
|
||||||
}
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncIntToValue")]
|
|
||||||
public void int_to_value (out Value value, int v);
|
|
||||||
[CCode (cname = "XSyncIntsToValue")]
|
|
||||||
public void ints_to_value (out Value value, uint l, int h);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncValueGreaterThan")]
|
|
||||||
public int value_greater_than (Value a, Value b);
|
|
||||||
[CCode (cname = "XSyncValueLessThan")]
|
|
||||||
public int value_less_than (Value a, Value b);
|
|
||||||
[CCode (cname = "XSyncValueGreaterOrEqual")]
|
|
||||||
public int value_greater_or_equal (Value a, Value b);
|
|
||||||
[CCode (cname = "XSyncValueLessOrEqual")]
|
|
||||||
public int value_less_or_equal (Value a, Value b);
|
|
||||||
[CCode (cname = "XSyncValueEqual")]
|
|
||||||
public int value_equal (Value a, Value b);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncValueIsNegative")]
|
|
||||||
public int value_is_negative (Value a, Value b);
|
|
||||||
[CCode (cname = "XSyncValueIsZero")]
|
|
||||||
public int value_is_zero (Value a, Value b);
|
|
||||||
[CCode (cname = "XSyncValueIsPositive")]
|
|
||||||
public int value_is_positive (Value a, Value b);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncValueLow32")]
|
|
||||||
public uint value_low32 (Value value);
|
|
||||||
[CCode (cname = "XSyncValueHigh32")]
|
|
||||||
public int value_high32 (Value value);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncValueAdd")]
|
|
||||||
public void value_add
|
|
||||||
(out Value result, Value a, Value b, out int poverflow);
|
|
||||||
[CCode (cname = "XSyncValueSubtract")]
|
|
||||||
public void value_subtract
|
|
||||||
(out Value result, Value a, Value b, out int poverflow);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncMaxValue")]
|
|
||||||
public void max_value (out Value pv);
|
|
||||||
[CCode (cname = "XSyncMinValue")]
|
|
||||||
public void min_value (out Value pv);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncSystemCounter", has_type_id = false)]
|
|
||||||
public struct SystemCounter {
|
|
||||||
public string name;
|
|
||||||
public X.ID counter;
|
|
||||||
public Value resolution;
|
|
||||||
}
|
|
||||||
[CCode (cname = "XSyncTrigger", has_type_id = false)]
|
|
||||||
public struct Trigger {
|
|
||||||
public X.ID counter;
|
|
||||||
public ValueType value_type;
|
|
||||||
public Value wait_value;
|
|
||||||
public TestType test_type;
|
|
||||||
}
|
|
||||||
[CCode (cname = "XSyncWaitCondition", has_type_id = false)]
|
|
||||||
public struct WaitCondition {
|
|
||||||
public Trigger trigger;
|
|
||||||
public Value event_threshold;
|
|
||||||
}
|
|
||||||
[CCode (cname = "XSyncAlarmAttributes", has_type_id = false)]
|
|
||||||
public struct AlarmAttributes {
|
|
||||||
public Trigger trigger;
|
|
||||||
public Value delta;
|
|
||||||
public int events;
|
|
||||||
public AlarmState state;
|
|
||||||
}
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncCounterNotifyEvent", has_type_id = false)]
|
|
||||||
public struct CounterNotifyEvent {
|
|
||||||
// TODO: other fields
|
|
||||||
public X.ID counter;
|
|
||||||
public Value wait_value;
|
|
||||||
public Value counter_value;
|
|
||||||
}
|
|
||||||
[CCode (cname = "XSyncAlarmNotifyEvent", has_type_id = false)]
|
|
||||||
public struct AlarmNotifyEvent {
|
|
||||||
// TODO: other fields
|
|
||||||
public X.ID alarm;
|
|
||||||
public Value counter_value;
|
|
||||||
public Value alarm_value;
|
|
||||||
public AlarmState state;
|
|
||||||
}
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncQueryExtension")]
|
|
||||||
public X.Status query_extension (X.Display dpy,
|
|
||||||
out int event_base, out int error_base);
|
|
||||||
[CCode (cname = "XSyncInitialize")]
|
|
||||||
public X.Status initialize (X.Display dpy,
|
|
||||||
out int major_version, out int minor_version);
|
|
||||||
[CCode (cname = "XSyncListSystemCounters")]
|
|
||||||
public SystemCounter *list_system_counters (X.Display dpy,
|
|
||||||
out int n_counters);
|
|
||||||
[CCode (cname = "XSyncFreeSystemCounterList")]
|
|
||||||
public void free_system_counter_list (SystemCounter *counters);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncCreateCounter")]
|
|
||||||
public X.ID create_counter (X.Display dpy, Value initial_value);
|
|
||||||
[CCode (cname = "XSyncSetCounter")]
|
|
||||||
public X.Status set_counter (X.Display dpy, X.ID counter, Value value);
|
|
||||||
[CCode (cname = "XSyncChangeCounter")]
|
|
||||||
public X.Status change_counter (X.Display dpy, X.ID counter, Value value);
|
|
||||||
[CCode (cname = "XSyncDestroyCounter")]
|
|
||||||
public X.Status destroy_counter (X.Display dpy, X.ID counter);
|
|
||||||
[CCode (cname = "XSyncQueryCounter")]
|
|
||||||
public X.Status query_counter (X.Display dpy,
|
|
||||||
X.ID counter, out Value value);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncAwait")]
|
|
||||||
public X.Status await (X.Display dpy,
|
|
||||||
WaitCondition *wait_list, int n_conditions);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncCreateAlarm")]
|
|
||||||
public X.ID create_alarm (X.Display dpy,
|
|
||||||
CA values_mask, ref AlarmAttributes values);
|
|
||||||
[CCode (cname = "XSyncDestroyAlarm")]
|
|
||||||
public X.Status destroy_alarm (X.Display dpy, X.ID alarm);
|
|
||||||
[CCode (cname = "XSyncQueryAlarm")]
|
|
||||||
public X.Status query_alarm (X.Display dpy,
|
|
||||||
X.ID alarm, out AlarmAttributes values_return);
|
|
||||||
[CCode (cname = "XSyncChangeAlarm")]
|
|
||||||
public X.Status change_alarm (X.Display dpy,
|
|
||||||
X.ID alarm, CA values_mask, ref AlarmAttributes values);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncSetPriority")]
|
|
||||||
public X.Status set_priority (X.Display dpy, X.ID alarm, int priority);
|
|
||||||
[CCode (cname = "XSyncGetPriority")]
|
|
||||||
public X.Status get_priority (X.Display dpy, X.ID alarm, out int priority);
|
|
||||||
|
|
||||||
[CCode (cname = "XSyncCreateFence")]
|
|
||||||
public X.ID create_fence (X.Display dpy,
|
|
||||||
X.Drawable d, int initially_triggered);
|
|
||||||
[CCode (cname = "XSyncTriggerFence")]
|
|
||||||
public int trigger_fence (X.Display dpy, X.ID fence);
|
|
||||||
[CCode (cname = "XSyncResetFence")]
|
|
||||||
public int reset_fence (X.Display dpy, X.ID fence);
|
|
||||||
[CCode (cname = "XSyncDestroyFence")]
|
|
||||||
public int destroy_fence (X.Display dpy, X.ID fence);
|
|
||||||
[CCode (cname = "XSyncQueryFence")]
|
|
||||||
public int query_fence (X.Display dpy, X.ID fence, out int triggered);
|
|
||||||
[CCode (cname = "XSyncAwaitFence")]
|
|
||||||
public int await_fence (X.Display dpy, X.ID *fence_list, int n_fences);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue