commit 71617d4b14e11557cf0a7c449cf2f21d33ea1113
Author: Přemysl Janouch
Date: Sun Oct 16 10:49:17 2016 +0200
Initial commit
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);
+ }
+}