From 71617d4b14e11557cf0a7c449cf2f21d33ea1113 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C5=99emysl=20Janouch?= 
Date: Sun, 16 Oct 2016 10:49:17 +0200
Subject: [PATCH] Initial commit
---
 .gitignore                 |   9 ++
 CMakeLists.txt             |  84 +++++++++++
 LICENSE                    |  15 ++
 README.adoc                |  54 +++++++
 cmake/FindVala.cmake       |  47 ++++++
 cmake/ValaPrecompile.cmake | 205 ++++++++++++++++++++++++++
 config.vala.in             |   8 ++
 wdmtg.vala                 | 288 +++++++++++++++++++++++++++++++++++++
 xsync.vapi                 | 243 +++++++++++++++++++++++++++++++
 9 files changed, 953 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 CMakeLists.txt
 create mode 100644 LICENSE
 create mode 100644 README.adoc
 create mode 100644 cmake/FindVala.cmake
 create mode 100644 cmake/ValaPrecompile.cmake
 create mode 100644 config.vala.in
 create mode 100644 wdmtg.vala
 create mode 100644 xsync.vapi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f0310d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+# Build files
+/build
+
+# Qt Creator files
+/CMakeLists.txt.user*
+/wdmtg.config
+/wdmtg.files
+/wdmtg.creator*
+/wdmtg.includes
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..465cc27
--- /dev/null
+++ b/CMakeLists.txt
@@ -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 ")
+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)
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ce263ae
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ Copyright (c) 2016, Přemysl Janouch 
+ 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.
+
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..4f9cb8e
--- /dev/null
+++ b/README.adoc
@@ -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 .
+
+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
diff --git a/cmake/FindVala.cmake b/cmake/FindVala.cmake
new file mode 100644
index 0000000..fd3d1c5
--- /dev/null
+++ b/cmake/FindVala.cmake
@@ -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)
+
diff --git a/cmake/ValaPrecompile.cmake b/cmake/ValaPrecompile.cmake
new file mode 100644
index 0000000..aa49ecc
--- /dev/null
+++ b/cmake/ValaPrecompile.cmake
@@ -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)
+
+
diff --git a/config.vala.in b/config.vala.in
new file mode 100644
index 0000000..036a42d
--- /dev/null
+++ b/config.vala.in
@@ -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@";
+}
+
diff --git a/wdmtg.vala b/wdmtg.vala
new file mode 100644
index 0000000..7c15dd5
--- /dev/null
+++ b/wdmtg.vala
@@ -0,0 +1,288 @@
+//
+// wdmtg.vala: activity tracker
+//
+// Copyright (c) 2016, Přemysl Janouch 
+//
+// 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;
+  }
+}
diff --git a/xsync.vapi b/xsync.vapi
new file mode 100644
index 0000000..9e77b16
--- /dev/null
+++ b/xsync.vapi
@@ -0,0 +1,243 @@
+//
+// xsync.vapi: selected XSync APIs
+//
+// Copyright (c) 2016, Přemysl Janouch 
+//
+// 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);
+  }
+}