Initial commit

This commit is contained in:
Přemysl Eric Janouch 2016-10-16 10:49:17 +02:00
commit 71617d4b14
Signed by: p
GPG Key ID: B715679E3A361BE6
9 changed files with 953 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Build files
/build
# Qt Creator files
/CMakeLists.txt.user*
/wdmtg.config
/wdmtg.files
/wdmtg.creator*
/wdmtg.includes

84
CMakeLists.txt Normal file
View File

@ -0,0 +1,84 @@
project (wdmtg C)
cmake_minimum_required (VERSION 2.8.12)
# Vala really sucks at producing good C code
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC)
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_GNUC)
# 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)
set (project_SHARE_DIR ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME})
set (project_INSTALL_SHARE_DIR share/${PROJECT_NAME})
endif (OPTION_NOINSTALL)
# Gather package information
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
find_package (Vala 0.12 REQUIRED)
find_package (PkgConfig REQUIRED)
pkg_check_modules (dependencies REQUIRED gtk+-3.0 sqlite3 x11 xext xextproto)
# Precompile Vala sources
include (ValaPrecompile)
set (HEADER_PATH "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.h")
set (CONFIG_PATH "${PROJECT_BINARY_DIR}/config.vala")
configure_file (${PROJECT_SOURCE_DIR}/config.vala.in ${CONFIG_PATH})
# I'm not sure what this was about, look at slovnik-gui for more comments
set (SYMBOLS_PATH "${PROJECT_BINARY_DIR}/${PROJECT_NAME}.def")
vala_init (${PROJECT_NAME}
PACKAGES gmodule-2.0 gio-2.0 gtk+-3.0 gee-0.8 sqlite3 x11
CUSTOM_VAPIS ${PROJECT_SOURCE_DIR}/xsync.vapi)
vala_add (${PROJECT_NAME} ${CONFIG_PATH})
vala_add (${PROJECT_NAME} ${PROJECT_NAME}.vala DEPENDS config)
vala_finish (${PROJECT_NAME}
SOURCES project_VALA_SOURCES
OUTPUTS project_VALA_C
GENERATE_HEADER ${HEADER_PATH}
GENERATE_SYMBOLS ${SYMBOLS_PATH})
# Include Vala sources as header files, so they appear in the IDE
# but CMake doesn't try to compile them directly
set_source_files_properties (${project_VALA_SOURCES}
PROPERTIES HEADER_FILE_ONLY TRUE)
set (project_SOURCES ${project_VALA_SOURCES} ${project_VALA_C} ${SYMBOLS_PATH})
# Build the executable and install it
include_directories (${dependencies_INCLUDE_DIRS})
link_directories (${dependencies_LIBRARY_DIRS})
add_executable (${PROJECT_NAME} ${project_SOURCES})
target_link_libraries (${PROJECT_NAME} ${dependencies_LIBRARIES})
install (TARGETS ${PROJECT_NAME} DESTINATION bin)
# CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Activity tracker")
set (CPACK_PACKAGE_VENDOR "Přemysl Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p.janouch@gmail.com>")
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}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/build;/\\\\.git")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}")
include (CPack)

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
All rights reserved.
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.

54
README.adoc Normal file
View File

@ -0,0 +1,54 @@
wdmtg
=====
'wdmtg' (Where Did My Time Go) is an automatic activity tracker for X11.
It tracks the current window title and whether the user seems to be idle.
Features
--------
Currently it's still under development, stuck in a proof-of-concept phase.
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR, or from
openSUSE Build Service for the rest of mainstream distributions. Consult the
list of repositories and their respective links at:
https://build.opensuse.org/project/repositories/home:pjanouch:git
Building and Running
--------------------
Build dependencies: CMake, pkg-config, Vala >= 0.12 +
Runtime dependencies: gtk+-3.0, sqlite3, x11, xextproto, xext
$ git clone --recursive https://github.com/pjanouch/wdmtg.git
$ mkdir wdmtg/build
$ cd wdmtg/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug
$ make
To install the application, you can do either the usual:
# make install
Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i wdmtg-*.deb
Contributing and Support
------------------------
Use this project's GitHub to report any bugs, request features, or submit pull
requests. If you want to discuss this project, or maybe just hang out with
the developer, feel free to join me at irc://irc.janouch.name, channel #dev.
License
-------
'wdmtg' is written by Přemysl Janouch <p.janouch@gmail.com>.
You may use the software under the terms of the ISC license, the text of which
is included within the package, or, at your option, you may relicense the work
under the MIT or the Modified BSD License, as listed at the following site:
http://www.gnu.org/licenses/license-list.html

47
cmake/FindVala.cmake Normal file
View File

@ -0,0 +1,47 @@
# - Find Vala
# This module looks for valac.
# This module defines the following values:
# VALA_FOUND
# VALA_COMPILER
# VALA_VERSION
#=============================================================================
# Copyright Přemysl Janouch 2011
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
#=============================================================================
find_program (VALA_COMPILER "valac")
if (VALA_COMPILER)
execute_process (COMMAND ${VALA_COMPILER} --version
OUTPUT_VARIABLE VALA_VERSION)
string (REGEX MATCH "[.0-9]+" VALA_VERSION "${VALA_VERSION}")
endif (VALA_COMPILER)
include (FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS (Vala
REQUIRED_VARS VALA_COMPILER
VERSION_VAR VALA_VERSION)
mark_as_advanced (VALA_COMPILER VALA_VERSION)

205
cmake/ValaPrecompile.cmake Normal file
View File

@ -0,0 +1,205 @@
# - Precompilation of Vala/Genie source files into C sources
# Makes use of the parallel build ability introduced in Vala 0.11. Derived
# from a similar module by Jakob Westhoff and the original GNU Make rules.
# Might be a bit oversimplified.
#
# This module defines three functions. The first one:
#
# vala_init (id
# [DIRECTORY dir] - Output directory (binary dir by default)
# [PACKAGES package...] - Package dependencies
# [OPTIONS option...] - Extra valac options
# [CUSTOM_VAPIS file...]) - Custom vapi files to include in the build
#
# initializes a single precompilation unit using the given arguments.
# You can put files into it via the following function:
#
# vala_add (id source.vala
# [DEPENDS source...]) - Vala/Genie source or .vapi dependencies
#
# Finally retrieve paths for generated C files by calling:
#
# vala_finish (id
# [SOURCES sources_var] - Input Vala/Genie sources
# [OUTPUTS outputs_var] - Output C sources
# [GENERATE_HEADER id.h - Generate id.h and id_internal.h
# [GENERATE_VAPI id.vapi] - Generate a vapi file
# [GENERATE_SYMBOLS id.def]]) - Generate a list of public symbols
#
#=============================================================================
# Copyright Přemysl Janouch 2011
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
#=============================================================================
find_package (Vala 0.11 REQUIRED)
include (CMakeParseArguments)
function (vala_init id)
set (_multi_value PACKAGES OPTIONS CUSTOM_VAPIS)
cmake_parse_arguments (arg "" "DIRECTORY" "${_multi_value}" ${ARGN})
if (arg_DIRECTORY)
set (directory ${arg_DIRECTORY})
if (NOT IS_DIRECTORY ${directory})
file (MAKE_DIRECTORY ${directory})
endif (NOT IS_DIRECTORY ${directory})
else (arg_DIRECTORY)
set (directory ${CMAKE_CURRENT_BINARY_DIR})
endif (arg_DIRECTORY)
set (pkg_opts)
foreach (pkg ${arg_PACKAGES})
list (APPEND pkg_opts "--pkg=${pkg}")
endforeach (pkg)
set (VALA_${id}_DIR "${directory}" PARENT_SCOPE)
set (VALA_${id}_ARGS ${pkg_opts} ${arg_OPTIONS}
${arg_CUSTOM_VAPIS} PARENT_SCOPE)
set (VALA_${id}_SOURCES "" PARENT_SCOPE)
set (VALA_${id}_OUTPUTS "" PARENT_SCOPE)
set (VALA_${id}_FAST_VAPI_FILES "" PARENT_SCOPE)
set (VALA_${id}_FAST_VAPI_ARGS "" PARENT_SCOPE)
endfunction (vala_init)
function (vala_add id file)
cmake_parse_arguments (arg "" "" "DEPENDS" ${ARGN})
if (NOT IS_ABSOLUTE "${file}")
set (file "${CMAKE_CURRENT_SOURCE_DIR}/${file}")
endif (NOT IS_ABSOLUTE "${file}")
get_filename_component (output_name "${file}" NAME)
get_filename_component (output_base "${file}" NAME_WE)
set (output_base "${VALA_${id}_DIR}/${output_base}")
# XXX: It would be best to have it working without touching the vapi
# but it appears this cannot be done in CMake.
add_custom_command (OUTPUT "${output_base}.vapi"
COMMAND ${VALA_COMPILER} "${file}" "--fast-vapi=${output_base}.vapi"
COMMAND ${CMAKE_COMMAND} -E touch "${output_base}.vapi"
DEPENDS "${file}"
COMMENT "Generating a fast vapi for ${output_name}" VERBATIM)
set (vapi_opts)
set (vapi_depends)
foreach (vapi ${arg_DEPENDS})
if (NOT IS_ABSOLUTE "${vapi}")
set (vapi "${VALA_${id}_DIR}/${vapi}.vapi")
endif (NOT IS_ABSOLUTE "${vapi}")
list (APPEND vapi_opts "--use-fast-vapi=${vapi}")
list (APPEND vapi_depends "${vapi}")
endforeach (vapi)
add_custom_command (OUTPUT "${output_base}.c"
COMMAND ${VALA_COMPILER} "${file}" -C ${vapi_opts} ${VALA_${id}_ARGS}
COMMAND ${CMAKE_COMMAND} -E touch "${output_base}.c"
DEPENDS "${file}" ${vapi_depends}
WORKING_DIRECTORY "${VALA_${id}_DIR}"
COMMENT "Precompiling ${output_name}" VERBATIM)
set (VALA_${id}_SOURCES ${VALA_${id}_SOURCES}
"${file}" PARENT_SCOPE)
set (VALA_${id}_OUTPUTS ${VALA_${id}_OUTPUTS}
"${output_base}.c" PARENT_SCOPE)
set (VALA_${id}_FAST_VAPI_FILES ${VALA_${id}_FAST_VAPI_FILES}
"${output_base}.vapi" PARENT_SCOPE)
set (VALA_${id}_FAST_VAPI_ARGS ${VALA_${id}_FAST_VAPI_ARGS}
"--use-fast-vapi=${output_base}.vapi" PARENT_SCOPE)
endfunction (vala_add)
function (vala_finish id)
set (_one_value SOURCES OUTPUTS
GENERATE_VAPI GENERATE_HEADER GENERATE_SYMBOLS)
cmake_parse_arguments (arg "" "${_one_value}" "" ${ARGN})
if (arg_SOURCES)
set (${arg_SOURCES} ${VALA_${id}_SOURCES} PARENT_SCOPE)
endif (arg_SOURCES)
if (arg_OUTPUTS)
set (${arg_OUTPUTS} ${VALA_${id}_OUTPUTS} PARENT_SCOPE)
endif (arg_OUTPUTS)
set (outputs)
set (export_args)
if (arg_GENERATE_VAPI)
if (NOT IS_ABSOLUTE "${arg_GENERATE_VAPI}")
set (arg_GENERATE_VAPI
"${VALA_${id}_DIR}/${arg_GENERATE_VAPI}")
endif (NOT IS_ABSOLUTE "${arg_GENERATE_VAPI}")
list (APPEND outputs "${arg_GENERATE_VAPI}")
list (APPEND export_args "--internal-vapi=${arg_GENERATE_VAPI}")
if (NOT arg_GENERATE_HEADER)
message (FATAL_ERROR "Header generation required for vapi")
endif (NOT arg_GENERATE_HEADER)
endif (arg_GENERATE_VAPI)
if (arg_GENERATE_SYMBOLS)
if (NOT IS_ABSOLUTE "${arg_GENERATE_SYMBOLS}")
set (arg_GENERATE_SYMBOLS
"${VALA_${id}_DIR}/${arg_GENERATE_SYMBOLS}")
endif (NOT IS_ABSOLUTE "${arg_GENERATE_SYMBOLS}")
list (APPEND outputs "${arg_GENERATE_SYMBOLS}")
list (APPEND export_args "--symbols=${arg_GENERATE_SYMBOLS}")
if (NOT arg_GENERATE_HEADER)
message (FATAL_ERROR "Header generation required for symbols")
endif (NOT arg_GENERATE_HEADER)
endif (arg_GENERATE_SYMBOLS)
if (arg_GENERATE_HEADER)
if (NOT IS_ABSOLUTE "${arg_GENERATE_HEADER}")
set (arg_GENERATE_HEADER
"${VALA_${id}_DIR}/${arg_GENERATE_HEADER}")
endif (NOT IS_ABSOLUTE "${arg_GENERATE_HEADER}")
get_filename_component (header_path "${arg_GENERATE_HEADER}" PATH)
get_filename_component (header_name "${arg_GENERATE_HEADER}" NAME_WE)
set (header_base "${header_path}/${header_name}")
get_filename_component (header_ext "${arg_GENERATE_HEADER}" EXT)
list (APPEND outputs
"${header_base}${header_ext}"
"${header_base}_internal${header_ext}")
list (APPEND export_args
"--header=${header_base}${header_ext}"
"--internal-header=${header_base}_internal${header_ext}")
endif (arg_GENERATE_HEADER)
if (outputs)
add_custom_command (OUTPUT ${outputs}
COMMAND ${VALA_COMPILER} -C ${VALA_${id}_ARGS}
${export_args} ${VALA_${id}_FAST_VAPI_ARGS}
DEPENDS ${VALA_${id}_FAST_VAPI_FILES}
COMMENT "Generating vapi/headers/symbols" VERBATIM)
endif (outputs)
endfunction (vala_finish id)

8
config.vala.in Normal file
View File

@ -0,0 +1,8 @@
[CCode (cprefix = "", lower_case_cprefix = "")]
namespace Config
{
public const string PROJECT_NAME = "${CMAKE_PROJECT_NAME}";
public const string PROJECT_VERSION = "${project_VERSION_MAJOR}.${project_VERSION_MINOR}.${project_VERSION_PATCH}";
public const string SHARE_DIR = "@project_SHARE_DIR@";
}

288
wdmtg.vala Normal file
View File

@ -0,0 +1,288 @@
//
// 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;
}
}

243
xsync.vapi Normal file
View File

@ -0,0 +1,243 @@
//
// xsync.vapi: selected XSync APIs
//
// 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
// TODO: move this outside, I didn't expect the Xlib binding to be this shitty
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;
}
// TODO: XSyncAlarmError
// TODO: XSyncCounterError
[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);
}
}