289 lines
9.8 KiB
Vala
289 lines
9.8 KiB
Vala
//
|
|
// wdmtg.vala: activity tracker
|
|
//
|
|
// Copyright (c) 2016, Přemysl 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, provided that the above
|
|
// copyright notice and this permission notice appear in all copies.
|
|
//
|
|
// 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 xsync 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
|
|
|
|
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
|
|
|
|
// --- 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 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)");
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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");
|
|
|
|
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");
|
|
set_idle_alarm (ref idle_alarm_inactive,
|
|
X.Sync.TestType.PositiveComparison, idle_timeout);
|
|
}
|
|
}
|
|
|
|
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 {
|
|
var ctx = new OptionContext (" - activity tracker");
|
|
ctx.set_help_enabled (true);
|
|
ctx.add_main_entries (options, null);
|
|
ctx.parse (ref args);
|
|
} catch (OptionError e) {
|
|
exit_fatal ("option parsing failed: %s", e.message);
|
|
return 1;
|
|
}
|
|
if (show_version) {
|
|
stdout.printf (Config.PROJECT_NAME + " " + Config.PROJECT_VERSION + "\n");
|
|
return 0;
|
|
}
|
|
|
|
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 sync_base, 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 loop = new MainLoop ();
|
|
var channel = new IOChannel.unix_new (dpy.connection_number ());
|
|
channel.add_watch (IOCondition.IN, (source, condition) => {
|
|
if (0 == (condition & IOCondition.IN))
|
|
return true;
|
|
|
|
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;
|
|
});
|
|
|
|
loop.run ();
|
|
return 0;
|
|
}
|
|
}
|