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); + } +}