summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2021-01-27 22:06:49 +0100
committerJoel Klinghed <the_jk@spawned.biz>2021-01-27 22:06:49 +0100
commit06950aab233de6a2f47293d59575bb42f6131660 (patch)
tree62f6eed4a6d35414f656d22b9ac7420849018a11
parent1ef9c463f1efc1adfb62e42ab3dd17e8c6394373 (diff)
Complete rewrite using C++ and with shared state support
-rw-r--r--.gitignore23
-rw-r--r--LICENSE25
-rw-r--r--Makefile.am7
-rw-r--r--README16
-rw-r--r--configure.ac99
-rw-r--r--data/org.the_jk.timer.desktop10
-rw-r--r--data/org.the_jk.timer.pngbin0 -> 2831 bytes
-rw-r--r--m4/ac_attribute.m447
-rw-r--r--m4/ax_append_compile_flags.m465
-rw-r--r--m4/ax_append_flag.m469
-rw-r--r--m4/ax_cflags_warn_all.m4122
-rw-r--r--m4/pkg.m4158
-rw-r--r--meson.build88
-rw-r--r--src/Makefile.am14
-rw-r--r--src/args.cc288
-rw-r--r--src/args.hh53
-rw-r--r--src/common.h17
-rw-r--r--src/common.hh6
-rw-r--r--src/compiler.h42
-rw-r--r--src/dynstr.c94
-rw-r--r--src/dynstr.h122
-rw-r--r--src/io.cc41
-rw-r--r--src/io.hh13
-rw-r--r--src/macros.h19
-rw-r--r--src/main.c896
-rw-r--r--src/paths.c1029
-rw-r--r--src/paths.h261
-rw-r--r--src/ref.h95
-rw-r--r--src/safe_fifo.c151
-rw-r--r--src/safe_fifo.h71
-rw-r--r--src/strutil.c123
-rw-r--r--src/strutil.h81
-rw-r--r--src/thread-pthread.c343
-rw-r--r--src/thread.h266
-rw-r--r--src/timer.cc555
-rw-r--r--src/timer_state.cc419
-rw-r--r--src/timer_state.hh64
-rw-r--r--src/timespec.c280
-rw-r--r--src/timespec.h26
-rw-r--r--src/unique_fd.cc17
-rw-r--r--src/unique_fd.hh50
-rw-r--r--src/xcb_atoms.cc75
-rw-r--r--src/xcb_atoms.hh57
-rw-r--r--src/xcb_colors.cc93
-rw-r--r--src/xcb_colors.hh57
-rw-r--r--src/xcb_connection.cc26
-rw-r--r--src/xcb_connection.hh30
-rw-r--r--src/xcb_event.hh32
-rw-r--r--src/xcb_resource.cc37
-rw-r--r--src/xcb_resource.hh105
-rw-r--r--src/xcb_resources.hh35
-rw-r--r--src/xcb_resources_none.cc35
-rw-r--r--src/xcb_resources_xrm.cc78
-rw-r--r--src/xcb_xkb.cc171
-rw-r--r--src/xcb_xkb.hh30
-rw-r--r--src/xdg.cc116
-rw-r--r--src/xdg.hh23
57 files changed, 2636 insertions, 4529 deletions
diff --git a/.gitignore b/.gitignore
index c9d63bb..567609b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,22 +1 @@
-.deps
-lib*.a
-*.analyze
-*.dSYM
-*.o
-*.plist
-Makefile
-Makefile.in
-/aclocal.m4
-/autom4te.cache
-/compile
-/config.h
-/config.h.in
-/config.h.in~
-/config.log
-/config.status
-/configure
-/depcomp
-/install-sh
-/missing
-/stamp-h1
-/test-driver \ No newline at end of file
+build/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d968dde
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+MIT License
+
+Copyright (c) 2021 Joel Klinghed
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+data/org.the_jk.timer.png:
+Creative Commons(Attribution 3.0 unported)
+Icon by Free Preloaders [https://freeicons.io/profile/726] on https://freeicons.io \ No newline at end of file
diff --git a/Makefile.am b/Makefile.am
deleted file mode 100644
index 29abe97..0000000
--- a/Makefile.am
+++ /dev/null
@@ -1,7 +0,0 @@
-ACLOCAL_AMFLAGS = -I m4
-
-MAINTAINERCLEANFILES = Makefile.in aclocal.m4 config.guess config.h.in \
- config.sub configure depcomp install-sh ltmain.sh \
- missing config.rpath mkinstalldirs
-
-SUBDIRS = src
diff --git a/README b/README
index 31fb960..271e300 100644
--- a/README
+++ b/README
@@ -1,11 +1,7 @@
-Some sources are missing, these are found in the sawmill project.
+Timer
+=====
-Specifically you need:
- lib/common/src/compiler.h
- lib/common/src/safe_fifo.h
- lib/common/src/timespec.h
- lib/common/src/thread.h
- lib/paths/src/paths.h
-
- lib/paths/src/libcommon.a
- lib/paths/src/libpaths.a \ No newline at end of file
+A fairly simple little timer to keep track of work hours.
+The new version uses dbus to allow multiple instances of
+timer to share state, useful when connecting remotely to
+VNC or similar. \ No newline at end of file
diff --git a/configure.ac b/configure.ac
deleted file mode 100644
index 1b85507..0000000
--- a/configure.ac
+++ /dev/null
@@ -1,99 +0,0 @@
-AC_INIT([timer], [0.1], [the_jk@yahoo.com])
-AC_CONFIG_MACRO_DIR([m4])
-
-AC_ISC_POSIX
-AC_HEADER_STDC
-AC_C_CONST
-AC_C_INLINE
-#AC_C_BIGENDIAN
-AC_C___ATTRIBUTE__
-#AC_PROG_RANLIB
-
-AM_INIT_AUTOMAKE([dist-bzip2 foreign color-tests parallel-tests])
-AM_SILENT_RULES([yes])
-AC_PROG_CC
-AM_PROG_CC_C_O
-
-DEFINES="-D_DEFAULT_SOURCE -D_GNU_SOURCE -D_XOPEN_SOURCE"
-AX_CFLAGS_WARN_ALL([DEFINES])
-AC_ARG_ENABLE([debug], AC_HELP_STRING([compile with debug options]),
- use_debug="$enableval",
- use_debug=no)
-if test "x$use_debug" = "xyes"; then
- DEFINES="$DEFINES -g -DDEBUG"
-else
- DEFINES="$DEFINES -DNDEBUG"
-fi
-AC_SUBST([DEFINES])
-
-# Common
-
-AC_HEADER_STDBOOL
-
-AC_SEARCH_LIBS([round], [m],, AC_MSG_ERROR([need round]))
-
-# Threads
-
-AC_CHECK_HEADER([pthread.h],have_pthread=1,have_pthread=0)
-if test "x$have_pthread" = "x1"; then
- OLDCFLAGS="$CFLAGS"
- OLDLDFLAGS="$LDFLAGS"
- CFLAGS="$OLDCFLAGS -pthread"
- LDFLAGS="$OLDLDFLAGS -pthread"
- AC_SEARCH_LIBS([pthread_create], [pthread],, have_pthread=0)
- LDFLAGS="$OLDLDFLAGS"
- CFLAGS="$OLDCFLAGS"
- if test "x$have_pthread" = "x1"; then
- DEFINES="$DEFINES -pthread"
- LIBS="$LIBS -pthread"
- fi
-
- OLDCFLAGS="$CFLAGS"
- CFLAGS="$OLDCFLAGS $DEFINES"
-
- AC_CHECK_FUNC([pthread_yield],[have_pthread_yield=1],[have_pthread_yield=0])
- AC_DEFINE_UNQUOTED([HAVE_PTHREAD_YIELD],[$have_pthread_yield],[define to 1 if pthread_yield is available])
-
- if test "x$have_pthread_yield" = "x1"; then
- AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
-#include <pthread.h>
-]],[[
-return pthread_yield();
-]])],pthread_yield_void=0,
- pthread_yield_void=1)
- AC_DEFINE_UNQUOTED([PTHREAD_YIELD_VOID],[$pthread_yield_void],[define to 1 if pthread_yield returns void])
- fi
- CFLAGS="$OLDCFLAGS"
-fi
-AM_CONDITIONAL([HAVE_PTHREAD], [test "x$have_pthread" = "x1"])
-AC_DEFINE_UNQUOTED([HAVE_PTHREAD],[$have_pthread],[define to 1 if pthread is available])
-
-if test "x$have_pthread" = "x0"; then
- AC_MSG_ERROR([no thread implementation found])
-fi
-
-# timeval
-AC_CHECK_TYPES([struct timespec],,,[[#include <time.h>]])
-
-# clock
-
-AC_SEARCH_LIBS([clock_gettime], [rt], [have_clock_gettime=1], [have_clock_gettime=0])
-AC_DEFINE_UNQUOTED([HAVE_CLOCK_GETTIME], [$have_clock_gettime], [Define to 1 if clock_gettime is available])
-
-# XCB
-
-PKG_PROG_PKG_CONFIG()
-
-xcb_modules="xcb xcb-event xcb-keysyms"
-PKG_CHECK_EXISTS([xcb-icccm >= 0.3.8],
- [have_xcb_icccm=1
- xcb_modules="$xcb_modules xcb-icccm"],[have_xcb_icccm=0])
-
-PKG_CHECK_MODULES([XCB], [$xcb_modules])
-
-AC_DEFINE_UNQUOTED([HAVE_XCB_ICCCM],[$have_xcb_icccm],[define to 1 if xcb-icccm is available])
-
-# Finish up
-
-AC_CONFIG_HEADERS([config.h])
-AC_OUTPUT([Makefile src/Makefile])
diff --git a/data/org.the_jk.timer.desktop b/data/org.the_jk.timer.desktop
new file mode 100644
index 0000000..7536e98
--- /dev/null
+++ b/data/org.the_jk.timer.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=Timer
+Comment=Keeps track of the hours
+Categories=Utility
+Exec=timer
+Icon=org.the_jk.timer
+StartupNotify=false
+StartupWMClass=org.the_jk.timer
diff --git a/data/org.the_jk.timer.png b/data/org.the_jk.timer.png
new file mode 100644
index 0000000..7bc15e8
--- /dev/null
+++ b/data/org.the_jk.timer.png
Binary files differ
diff --git a/m4/ac_attribute.m4 b/m4/ac_attribute.m4
deleted file mode 100644
index 23479a9..0000000
--- a/m4/ac_attribute.m4
+++ /dev/null
@@ -1,47 +0,0 @@
-dnl Copyright (C) 2004-2008 Kim Woelders
-dnl Copyright (C) 2008 Vincent Torri <vtorri at univ-evry dot fr>
-dnl That code is public domain and can be freely used or copied.
-dnl Originally snatched from somewhere...
-
-dnl Macro for checking if the compiler supports __attribute__
-
-dnl Usage: AC_C___ATTRIBUTE__
-dnl call AC_DEFINE for HAVE___ATTRIBUTE__ and __UNUSED__
-dnl if the compiler supports __attribute__, HAVE___ATTRIBUTE__ is
-dnl defined to 1 and __UNUSED__ is defined to __attribute__((unused))
-dnl otherwise, HAVE___ATTRIBUTE__ is not defined and __UNUSED__ is
-dnl defined to nothing.
-
-AC_DEFUN([AC_C___ATTRIBUTE__],
-[
-
-AC_MSG_CHECKING([for __attribute__])
-
-AC_CACHE_VAL([ac_cv___attribute__],
- [AC_TRY_COMPILE(
- [
-#include <stdlib.h>
-
-int func(int x);
-int foo(int x __attribute__ ((unused)))
-{
- exit(1);
-}
- ],
- [],
- [ac_cv___attribute__="yes"],
- [ac_cv___attribute__="no"]
- )])
-
-AC_MSG_RESULT($ac_cv___attribute__)
-
-if test "x${ac_cv___attribute__}" = "xyes" ; then
- AC_DEFINE([HAVE___ATTRIBUTE__], [1], [Define to 1 if your compiler has __attribute__])
- AC_DEFINE([__UNUSED__], [__attribute__((unused))], [Macro declaring a function argument to be unused])
- else
- AC_DEFINE([__UNUSED__], [], [Macro declaring a function argument to be unused])
-fi
-
-])
-
-dnl End of ac_attribute.m4
diff --git a/m4/ax_append_compile_flags.m4 b/m4/ax_append_compile_flags.m4
deleted file mode 100644
index 1f8e708..0000000
--- a/m4/ax_append_compile_flags.m4
+++ /dev/null
@@ -1,65 +0,0 @@
-# ===========================================================================
-# http://www.gnu.org/software/autoconf-archive/ax_append_compile_flags.html
-# ===========================================================================
-#
-# SYNOPSIS
-#
-# AX_APPEND_COMPILE_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS])
-#
-# DESCRIPTION
-#
-# For every FLAG1, FLAG2 it is checked whether the compiler works with the
-# flag. If it does, the flag is added FLAGS-VARIABLE
-#
-# If FLAGS-VARIABLE is not specified, the current language's flags (e.g.
-# CFLAGS) is used. During the check the flag is always added to the
-# current language's flags.
-#
-# If EXTRA-FLAGS is defined, it is added to the current language's default
-# flags (e.g. CFLAGS) when the check is done. The check is thus made with
-# the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to
-# force the compiler to issue an error when a bad flag is given.
-#
-# NOTE: This macro depends on the AX_APPEND_FLAG and
-# AX_CHECK_COMPILE_FLAG. Please keep this macro in sync with
-# AX_APPEND_LINK_FLAGS.
-#
-# LICENSE
-#
-# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
-#
-# This program is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-# As a special exception, the respective Autoconf Macro's copyright owner
-# gives unlimited permission to copy, distribute and modify the configure
-# scripts that are the output of Autoconf when processing the Macro. You
-# need not follow the terms of the GNU General Public License when using
-# or distributing such scripts, even though portions of the text of the
-# Macro appear in them. The GNU General Public License (GPL) does govern
-# all other use of the material that constitutes the Autoconf Macro.
-#
-# This special exception to the GPL applies to versions of the Autoconf
-# Macro released by the Autoconf Archive. When you make and distribute a
-# modified version of the Autoconf Macro, you may extend this special
-# exception to the GPL to apply to your modified version as well.
-
-#serial 3
-
-AC_DEFUN([AX_APPEND_COMPILE_FLAGS],
-[AC_REQUIRE([AX_CHECK_COMPILE_FLAG])
-AC_REQUIRE([AX_APPEND_FLAG])
-for flag in $1; do
- AX_CHECK_COMPILE_FLAG([$flag], [AX_APPEND_FLAG([$flag], [$2])], [], [$3])
-done
-])dnl AX_APPEND_COMPILE_FLAGS
diff --git a/m4/ax_append_flag.m4 b/m4/ax_append_flag.m4
deleted file mode 100644
index 1d38b76..0000000
--- a/m4/ax_append_flag.m4
+++ /dev/null
@@ -1,69 +0,0 @@
-# ===========================================================================
-# http://www.gnu.org/software/autoconf-archive/ax_append_flag.html
-# ===========================================================================
-#
-# SYNOPSIS
-#
-# AX_APPEND_FLAG(FLAG, [FLAGS-VARIABLE])
-#
-# DESCRIPTION
-#
-# FLAG is appended to the FLAGS-VARIABLE shell variable, with a space
-# added in between.
-#
-# If FLAGS-VARIABLE is not specified, the current language's flags (e.g.
-# CFLAGS) is used. FLAGS-VARIABLE is not changed if it already contains
-# FLAG. If FLAGS-VARIABLE is unset in the shell, it is set to exactly
-# FLAG.
-#
-# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION.
-#
-# LICENSE
-#
-# Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
-# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
-#
-# This program is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-# As a special exception, the respective Autoconf Macro's copyright owner
-# gives unlimited permission to copy, distribute and modify the configure
-# scripts that are the output of Autoconf when processing the Macro. You
-# need not follow the terms of the GNU General Public License when using
-# or distributing such scripts, even though portions of the text of the
-# Macro appear in them. The GNU General Public License (GPL) does govern
-# all other use of the material that constitutes the Autoconf Macro.
-#
-# This special exception to the GPL applies to versions of the Autoconf
-# Macro released by the Autoconf Archive. When you make and distribute a
-# modified version of the Autoconf Macro, you may extend this special
-# exception to the GPL to apply to your modified version as well.
-
-#serial 2
-
-AC_DEFUN([AX_APPEND_FLAG],
-[AC_PREREQ(2.59)dnl for _AC_LANG_PREFIX
-AS_VAR_PUSHDEF([FLAGS], [m4_default($2,_AC_LANG_PREFIX[FLAGS])])dnl
-AS_VAR_SET_IF(FLAGS,
- [case " AS_VAR_GET(FLAGS) " in
- *" $1 "*)
- AC_RUN_LOG([: FLAGS already contains $1])
- ;;
- *)
- AC_RUN_LOG([: FLAGS="$FLAGS $1"])
- AS_VAR_SET(FLAGS, ["AS_VAR_GET(FLAGS) $1"])
- ;;
- esac],
- [AS_VAR_SET(FLAGS,["$1"])])
-AS_VAR_POPDEF([FLAGS])dnl
-])dnl AX_APPEND_FLAG
diff --git a/m4/ax_cflags_warn_all.m4 b/m4/ax_cflags_warn_all.m4
deleted file mode 100644
index 0fa3e18..0000000
--- a/m4/ax_cflags_warn_all.m4
+++ /dev/null
@@ -1,122 +0,0 @@
-# ===========================================================================
-# http://www.gnu.org/software/autoconf-archive/ax_cflags_warn_all.html
-# ===========================================================================
-#
-# SYNOPSIS
-#
-# AX_CFLAGS_WARN_ALL [(shellvar [,default, [A/NA]])]
-# AX_CXXFLAGS_WARN_ALL [(shellvar [,default, [A/NA]])]
-# AX_FCFLAGS_WARN_ALL [(shellvar [,default, [A/NA]])]
-#
-# DESCRIPTION
-#
-# Try to find a compiler option that enables most reasonable warnings.
-#
-# For the GNU compiler it will be -Wall (and -ansi -pedantic) The result
-# is added to the shellvar being CFLAGS, CXXFLAGS, or FCFLAGS by default.
-#
-# Currently this macro knows about the GCC, Solaris, Digital Unix, AIX,
-# HP-UX, IRIX, NEC SX-5 (Super-UX 10), Cray J90 (Unicos 10.0.0.8), and
-# Intel compilers. For a given compiler, the Fortran flags are much more
-# experimental than their C equivalents.
-#
-# - $1 shell-variable-to-add-to : CFLAGS, CXXFLAGS, or FCFLAGS
-# - $2 add-value-if-not-found : nothing
-# - $3 action-if-found : add value to shellvariable
-# - $4 action-if-not-found : nothing
-#
-# NOTE: These macros depend on AX_APPEND_FLAG.
-#
-# LICENSE
-#
-# Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
-# Copyright (c) 2010 Rhys Ulerich <rhys.ulerich@gmail.com>
-#
-# This program is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation; either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-# Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-# As a special exception, the respective Autoconf Macro's copyright owner
-# gives unlimited permission to copy, distribute and modify the configure
-# scripts that are the output of Autoconf when processing the Macro. You
-# need not follow the terms of the GNU General Public License when using
-# or distributing such scripts, even though portions of the text of the
-# Macro appear in them. The GNU General Public License (GPL) does govern
-# all other use of the material that constitutes the Autoconf Macro.
-#
-# This special exception to the GPL applies to versions of the Autoconf
-# Macro released by the Autoconf Archive. When you make and distribute a
-# modified version of the Autoconf Macro, you may extend this special
-# exception to the GPL to apply to your modified version as well.
-
-#serial 14
-
-AC_DEFUN([AX_FLAGS_WARN_ALL],[dnl
-AS_VAR_PUSHDEF([FLAGS],[_AC_LANG_PREFIX[]FLAGS])dnl
-AS_VAR_PUSHDEF([VAR],[ac_cv_[]_AC_LANG_ABBREV[]flags_warn_all])dnl
-AC_CACHE_CHECK([m4_ifval($1,$1,FLAGS) for maximum warnings],
-VAR,[VAR="no, unknown"
-ac_save_[]FLAGS="$[]FLAGS"
-for ac_arg dnl
-in "-warn all % -warn all" dnl Intel
- "-pedantic % -Wall" dnl GCC
- "-xstrconst % -v" dnl Solaris C
- "-std1 % -verbose -w0 -warnprotos" dnl Digital Unix
- "-qlanglvl=ansi % -qsrcmsg -qinfo=all:noppt:noppc:noobs:nocnd" dnl AIX
- "-ansi -ansiE % -fullwarn" dnl IRIX
- "+ESlit % +w1" dnl HP-UX C
- "-Xc % -pvctl[,]fullmsg" dnl NEC SX-5 (Super-UX 10)
- "-h conform % -h msglevel 2" dnl Cray C (Unicos)
- #
-do FLAGS="$ac_save_[]FLAGS "`echo $ac_arg | sed -e 's,%%.*,,' -e 's,%,,'`
- AC_COMPILE_IFELSE([AC_LANG_PROGRAM],
- [VAR=`echo $ac_arg | sed -e 's,.*% *,,'` ; break])
-done
-FLAGS="$ac_save_[]FLAGS"
-])
-AS_VAR_POPDEF([FLAGS])dnl
-AC_REQUIRE([AX_APPEND_FLAG])
-case ".$VAR" in
- .ok|.ok,*) m4_ifvaln($3,$3) ;;
- .|.no|.no,*) m4_default($4,[m4_ifval($2,[AX_APPEND_FLAG([$2], [$1])])]) ;;
- *) m4_default($3,[AX_APPEND_FLAG([$VAR], [$1])]) ;;
-esac
-AS_VAR_POPDEF([VAR])dnl
-])dnl AX_FLAGS_WARN_ALL
-dnl implementation tactics:
-dnl the for-argument contains a list of options. The first part of
-dnl these does only exist to detect the compiler - usually it is
-dnl a global option to enable -ansi or -extrawarnings. All other
-dnl compilers will fail about it. That was needed since a lot of
-dnl compilers will give false positives for some option-syntax
-dnl like -Woption or -Xoption as they think of it is a pass-through
-dnl to later compile stages or something. The "%" is used as a
-dnl delimiter. A non-option comment can be given after "%%" marks
-dnl which will be shown but not added to the respective C/CXXFLAGS.
-
-AC_DEFUN([AX_CFLAGS_WARN_ALL],[dnl
-AC_LANG_PUSH([C])
-AX_FLAGS_WARN_ALL([$1], [$2], [$3], [$4])
-AC_LANG_POP([C])
-])
-
-AC_DEFUN([AX_CXXFLAGS_WARN_ALL],[dnl
-AC_LANG_PUSH([C++])
-AX_FLAGS_WARN_ALL([$1], [$2], [$3], [$4])
-AC_LANG_POP([C++])
-])
-
-AC_DEFUN([AX_FCFLAGS_WARN_ALL],[dnl
-AC_LANG_PUSH([Fortran])
-AX_FLAGS_WARN_ALL([$1], [$2], [$3], [$4])
-AC_LANG_POP([Fortran])
-])
diff --git a/m4/pkg.m4 b/m4/pkg.m4
deleted file mode 100644
index bfe1dd1..0000000
--- a/m4/pkg.m4
+++ /dev/null
@@ -1,158 +0,0 @@
-# pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*-
-#
-# Copyright © 2004 Scott James Remnant <scott@netsplit.com>.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-#
-# As a special exception to the GNU General Public License, if you
-# distribute this file as part of a program that contains a
-# configuration script generated by Autoconf, you may include it under
-# the same distribution terms that you use for the rest of that program.
-
-# PKG_PROG_PKG_CONFIG([MIN-VERSION])
-# ----------------------------------
-AC_DEFUN([PKG_PROG_PKG_CONFIG],
-[m4_pattern_forbid([^_?PKG_[A-Z_]+$])
-m4_pattern_allow([^PKG_CONFIG(_PATH)?$])
-AC_ARG_VAR([PKG_CONFIG], [path to pkg-config utility])dnl
-if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then
- AC_PATH_TOOL([PKG_CONFIG], [pkg-config])
-fi
-if test -n "$PKG_CONFIG"; then
- _pkg_min_version=m4_default([$1], [0.9.0])
- AC_MSG_CHECKING([pkg-config is at least version $_pkg_min_version])
- if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then
- AC_MSG_RESULT([yes])
- else
- AC_MSG_RESULT([no])
- PKG_CONFIG=""
- fi
-
-fi[]dnl
-])# PKG_PROG_PKG_CONFIG
-
-# PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])
-#
-# Check to see whether a particular set of modules exists. Similar
-# to PKG_CHECK_MODULES(), but does not set variables or print errors.
-#
-#
-# Similar to PKG_CHECK_MODULES, make sure that the first instance of
-# this or PKG_CHECK_MODULES is called, or make sure to call
-# PKG_CHECK_EXISTS manually
-# --------------------------------------------------------------
-AC_DEFUN([PKG_CHECK_EXISTS],
-[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl
-if test -n "$PKG_CONFIG" && \
- AC_RUN_LOG([$PKG_CONFIG --exists --print-errors "$1"]); then
- m4_ifval([$2], [$2], [:])
-m4_ifvaln([$3], [else
- $3])dnl
-fi])
-
-
-# _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES])
-# ---------------------------------------------
-m4_define([_PKG_CONFIG],
-[if test -n "$PKG_CONFIG"; then
- if test -n "$$1"; then
- pkg_cv_[]$1="$$1"
- else
- PKG_CHECK_EXISTS([$3],
- [pkg_cv_[]$1=`$PKG_CONFIG --[]$2 "$3" 2>/dev/null`],
- [pkg_failed=yes])
- fi
-else
- pkg_failed=untried
-fi[]dnl
-])# _PKG_CONFIG
-
-# _PKG_SHORT_ERRORS_SUPPORTED
-# -----------------------------
-AC_DEFUN([_PKG_SHORT_ERRORS_SUPPORTED],
-[AC_REQUIRE([PKG_PROG_PKG_CONFIG])
-if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
- _pkg_short_errors_supported=yes
-else
- _pkg_short_errors_supported=no
-fi[]dnl
-])# _PKG_SHORT_ERRORS_SUPPORTED
-
-
-# PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND],
-# [ACTION-IF-NOT-FOUND])
-#
-#
-# Note that if there is a possibility the first call to
-# PKG_CHECK_MODULES might not happen, you should be sure to include an
-# explicit call to PKG_PROG_PKG_CONFIG in your configure.ac
-#
-#
-# --------------------------------------------------------------
-AC_DEFUN([PKG_CHECK_MODULES],
-[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl
-AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl
-AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl
-
-pkg_failed=no
-AC_MSG_CHECKING([for $2])
-
-_PKG_CONFIG([$1][_CFLAGS], [cflags], [$2])
-_PKG_CONFIG([$1][_LIBS], [libs], [$2])
-
-m4_define([_PKG_TEXT], [Alternatively, you may set the environment variables $1[]_CFLAGS
-and $1[]_LIBS to avoid the need to call pkg-config.
-See the pkg-config man page for more details.])
-
-if test $pkg_failed = yes; then
- _PKG_SHORT_ERRORS_SUPPORTED
- if test $_pkg_short_errors_supported = yes; then
- $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --errors-to-stdout --print-errors "$2"`
- else
- $1[]_PKG_ERRORS=`$PKG_CONFIG --errors-to-stdout --print-errors "$2"`
- fi
- # Put the nasty error message in config.log where it belongs
- echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD
-
- ifelse([$4], , [AC_MSG_ERROR(dnl
-[Package requirements ($2) were not met:
-
-$$1_PKG_ERRORS
-
-Consider adjusting the PKG_CONFIG_PATH environment variable if you
-installed software in a non-standard prefix.
-
-_PKG_TEXT
-])],
- [$4])
- AC_MSG_RESULT([no])
-elif test $pkg_failed = untried; then
- ifelse([$4], , [AC_MSG_FAILURE(dnl
-[The pkg-config script could not be found or is too old. Make sure it
-is in your PATH or set the PKG_CONFIG environment variable to the full
-path to pkg-config.
-
-_PKG_TEXT
-
-To get pkg-config, see <http://www.freedesktop.org/software/pkgconfig>.])],
- [$4])
- AC_MSG_RESULT([no])
-else
- $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS
- $1[]_LIBS=$pkg_cv_[]$1[]_LIBS
- AC_MSG_RESULT([yes])
- ifelse([$3], , :, [$3])
-fi[]dnl
-])# PKG_CHECK_MODULES
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..12495da
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,88 @@
+project('timer', 'cpp',
+ version: '0.2',
+ default_options: [
+ 'warning_level=3',
+ 'cpp_std=c++17',
+ 'cpp_rtti=false'
+ ])
+
+cpp_optional_flags = [
+ '-fvisibility=hidden',
+ # Redefining explicit to be able to include xkb.h
+ '-Wno-keyword-macro',
+]
+cpp_flags = [
+ '-DVERSION="' + meson.project_version() + '"'
+]
+if get_option('buildtype') == 'release'
+ cpp_flags += '-DNDEBUG'
+endif
+
+cpp = meson.get_compiler('cpp')
+cpp_flags += cpp.get_supported_arguments(cpp_optional_flags)
+add_project_arguments(cpp_flags, language: 'cpp')
+
+thread_dep = dependency('threads')
+
+xcb_dep = [dependency('xcb', version: '>= 1.14'),
+ dependency('xcb-xkb', version: '>= 1.14'),
+ dependency('xcb-event', version: '>= 0.4.0'),
+ dependency('xcb-icccm', version: '>= 0.4.1'),
+ dependency('xcb-keysyms', version: '>= 0.4.0'),
+ dependency('xkbcommon-x11', version: '>= 1.0.3')]
+
+xcb_xrm_dep = dependency('xcb-xrm', version: '>= 1.3', required: false)
+
+dbus_dep = dependency('sdbus-c++', version: '>= 0.8.3')
+
+timer_sources = [
+ 'src/args.cc',
+ 'src/io.cc',
+ 'src/timer.cc',
+ 'src/timer_state.cc',
+ 'src/unique_fd.cc',
+ 'src/xcb_atoms.cc',
+ 'src/xcb_colors.cc',
+ 'src/xcb_connection.cc',
+ 'src/xcb_resource.cc',
+ 'src/xcb_xkb.cc',
+ 'src/xdg.cc',
+]
+
+if xcb_xrm_dep.found()
+ timer_sources += 'src/xcb_resources_xrm.cc'
+else
+ timer_sources += 'src/xcb_resources_none.cc'
+endif
+
+exe = executable('timer',
+ sources: timer_sources,
+ dependencies: [dbus_dep, thread_dep, xcb_dep, xcb_xrm_dep],
+ install: true)
+
+xdg_desktop_menu = find_program('xdg-desktop-menu', required: false,
+ native: true)
+if xdg_desktop_menu.found()
+ meson.add_install_script(xdg_desktop_menu, 'install', '--novendor',
+ files('data/org.the_jk.timer.desktop'))
+else
+ desktop_path = get_option('datadir') / 'applications'
+
+ install_data('data/org.the_jk.timer.desktop', install_dir : desktop_path)
+endif
+
+xdg_icon_resource = find_program('xdg-icon-resource', required: false,
+ native: true)
+if xdg_icon_resource.found()
+ meson.add_install_script(xdg_icon_resource, 'install', '--novendor',
+ '--size', '128', files('data/org.the_jk.timer.png'))
+else
+ icon_path = get_option('datadir') / 'icons' / 'hicolor' / '128x128' / 'apps'
+ install_data('data/org.the_jk.timer.png', install_dir : icon_path)
+
+ gtk_update_icon_cache = find_program('gtk-update-icon-cache',
+ required: false, native: true)
+ if gtk_update_icon_cache.found()
+ meson.add_install_script(gtk_update_icon_cache, icon_path)
+ endif
+endif
diff --git a/src/Makefile.am b/src/Makefile.am
deleted file mode 100644
index 7131b86..0000000
--- a/src/Makefile.am
+++ /dev/null
@@ -1,14 +0,0 @@
-MAINTAINERCLEANFILES = Makefile.in
-
-AM_CPPFLAGS = @DEFINES@ -DVERSION='"@VERSION@"'
-
-bin_PROGRAMS = timer
-
-timer_SOURCES = main.c common.h compiler.h safe_fifo.h thread.h paths.h \
- safe_fifo.c timespec.h timespec.c paths.c ref.h macros.h \
- strutil.h strutil.c dynstr.h dynstr.c
-if HAVE_PTHREAD
-timer_SOURCES += thread-pthread.c
-endif
-timer_CFLAGS = @XCB_CFLAGS@
-timer_LDADD = @XCB_LIBS@
diff --git a/src/args.cc b/src/args.cc
new file mode 100644
index 0000000..50f6c5a
--- /dev/null
+++ b/src/args.cc
@@ -0,0 +1,288 @@
+#include "common.hh"
+
+#include "args.hh"
+
+#include <iostream>
+#include <unordered_map>
+#include <vector>
+
+namespace {
+
+class OptionImpl : public Option {
+public:
+ OptionImpl(char short_name, std::string long_name, std::string description,
+ bool require_arg, std::string arg_description)
+ : short_name_(short_name),
+ long_name_(std::move(long_name)),
+ description_(std::move(description)),
+ require_arg_(require_arg),
+ arg_description_(std::move(arg_description)) {
+ }
+
+ bool is_set() const override { return set_; }
+
+ std::string const& arg() const override { return arg_; }
+
+ char short_name() const { return short_name_; }
+
+ std::string const& long_name() const { return long_name_; }
+
+ std::string const& description() const { return description_; }
+
+ bool require_arg() const { return require_arg_; }
+
+ std::string const& arg_description() const { return arg_description_; }
+
+ void reset() {
+ set_ = false;
+ arg_.clear();
+ }
+
+ void set() {
+ set_ = true;
+ }
+
+ void set_arg(std::string arg) {
+ arg_ = std::move(arg);
+ }
+
+private:
+ char const short_name_;
+ std::string const long_name_;
+ std::string const description_;
+ bool const require_arg_;
+ std::string const arg_description_;
+ bool set_ = false;
+ std::string arg_;
+};
+
+class ArgsImpl : public Args {
+public:
+ ArgsImpl() = default;
+
+ Option const* add_option(char short_name, std::string long_name,
+ std::string description) override {
+ prepare_option(short_name, long_name);
+ options_.push_back(std::make_unique<OptionImpl>(short_name,
+ std::move(long_name),
+ std::move(description),
+ false, std::string()));
+ return options_.back().get();
+ }
+
+ Option const* add_option_with_arg(char short_name, std::string long_name,
+ std::string description,
+ std::string arg_description) override {
+ prepare_option(short_name, long_name);
+ options_.push_back(std::make_unique<OptionImpl>(short_name,
+ std::move(long_name),
+ std::move(description),
+ true, arg_description));
+ return options_.back().get();
+ }
+
+ bool run(int argc, char** argv, std::string_view prgname, std::ostream& err,
+ std::vector<std::string>* out) override {
+ for (int a = 1; a < argc; ++a) {
+ if (argv[a][0] == '-') {
+ if (argv[a][1] == '-') {
+ if (argv[a][2] != '\0') {
+ // A long name with optional "=" argument
+ size_t len = 2;
+ while (argv[a][len] != '=' && argv[a][len])
+ ++len;
+ std::string name(argv[a] + 2, len - 2);
+ auto it = long_names_.find(name);
+ if (it == long_names_.end()) {
+ err << prgname << ": unrecognized option '--"
+ << name << "'" << std::endl;
+ return false;
+ }
+ auto* opt = options_[it->second].get();
+ opt->set();
+ if (argv[a][len]) {
+ if (opt->require_arg()) {
+ opt->set_arg(std::string(argv[a] + len + 1));
+ } else {
+ err << prgname << ": option '--"
+ << name << "' doesn't allow an argument" << std::endl;
+ return false;
+ }
+ } else {
+ if (opt->require_arg()) {
+ if (a + 1 >= argc) {
+ err << prgname << ": option '--"
+ << name << "' requires an argument" << std::endl;
+ return false;
+ } else {
+ opt->set_arg(argv[++a]);
+ }
+ }
+ }
+ continue;
+ } else {
+ // "--", all following values are arguments
+ for (++a; a < argc; ++a)
+ out->push_back(argv[a]);
+ break;
+ }
+ } else if (argv[a][1] != '\0') {
+ // One or more short names
+ for (auto* name = argv[a] + 1; *name; ++name) {
+ auto it = short_names_.find(*name);
+ if (it == short_names_.end()) {
+ err << prgname << ": invalid option -- '"
+ << *name << "'" << std::endl;
+ return false;
+ }
+ auto* opt = options_[it->second].get();
+ opt->set();
+ if (opt->require_arg()) {
+ if (a + 1 >= argc) {
+ err << prgname << ": option requires an argument"
+ << " -- '" << *name << "'" << std::endl;
+ return false;
+ } else {
+ opt->set_arg(argv[++a]);
+ }
+ }
+ }
+ continue;
+ } else {
+ // single "-", treat as argument
+ }
+ }
+
+ out->push_back(argv[a]);
+ }
+ return true;
+ }
+
+ void print_descriptions(std::ostream& out,
+ uint32_t column_width) const override {
+ uint32_t max_left = 0;
+ for (auto const& option : options_) {
+ uint32_t left = 0;
+ if (option->short_name() != '\0') {
+ if (!option->long_name().empty()) {
+ left = 6 + option->long_name().size(); // -S, --long
+ } else {
+ left = 2; // -S
+ }
+ } else if (!option->long_name().empty()) {
+ left = 2 + option->long_name().size(); // --long
+ }
+ if (option->require_arg())
+ left += 1 + option->arg_description().size(); // (=| )ARG
+ if (left > 0)
+ left += 2; // Need at least two spaces between option and desc
+
+ if (left > max_left)
+ max_left = left;
+ }
+
+ uint32_t const avail_right =
+ max_left > column_width ? 0 : column_width - max_left;
+
+ if (avail_right < 20) {
+ // Fallback mode, description on its own row.
+ for (auto const& option : options_) {
+ print_option(out, *option);
+ out << '\n' << option->description() << '\n';
+ }
+ return;
+ }
+
+ // Check if all descriptions fit, justify to the right on a 80 col width
+ bool all_desc_fit = true;
+ uint32_t max_right = 0;
+ for (auto const& option : options_) {
+ uint32_t right = option->description().size();
+ if (right > avail_right) {
+ all_desc_fit = false;
+ break;
+ }
+ if (right > max_right)
+ max_right = right;
+ }
+
+ if (all_desc_fit)
+ max_left = std::max(80u, column_width) - max_right;
+
+ for (auto const& option : options_) {
+ uint32_t left = print_option(out, *option);
+ std::fill_n(std::ostreambuf_iterator<char>(out), max_left - left, ' ');
+
+ if (option->description().size() <= avail_right) {
+ out << option->description() << '\n';
+ continue;
+ }
+
+ // Wrap description
+ size_t last = 0;
+ bool first = true;
+ while (true) {
+ if (first) {
+ first = false;
+ } else {
+ std::fill_n(std::ostreambuf_iterator<char>(out), max_left, ' ');
+ }
+
+ size_t end = last + avail_right;
+ if (end >= option->description().size()) {
+ out << option->description() << '\n';
+ break;
+ }
+ size_t space = option->description().rfind(' ', end);
+ if (space == std::string::npos || space < last) {
+ space = end;
+ }
+ out << option->description().substr(last, space - last) << '\n';
+ last = space < end ? space + 1 : end;
+ }
+ }
+ }
+
+private:
+ void prepare_option(char short_name, std::string const& long_name) {
+ if (short_name != '\0')
+ short_names_.emplace(short_name, options_.size());
+ if (!long_name.empty()) {
+ assert(long_name.find('=') == std::string::npos);
+ long_names_.emplace(long_name, options_.size());
+ }
+ }
+
+ size_t print_option(std::ostream& out, const OptionImpl& option) const {
+ bool only_short = false;
+ size_t ret = 0;
+ if (option.short_name() != '\0') {
+ out << '-' << option.short_name();
+ if (!option.long_name().empty()) {
+ out << ", --" << option.long_name();
+ ret = 6 + option.long_name().size();
+ } else {
+ ret = 2;
+ only_short = true;
+ }
+ } else if (!option.long_name().empty()) {
+ out << "--" << option.long_name();
+ ret = 2 + option.long_name().size();
+ }
+ if (option.require_arg()) {
+ out << (only_short ? ' ' : '=') << option.arg_description();
+ ret += 1 + option.arg_description().size();
+ }
+ return ret;
+ }
+
+ std::vector<std::unique_ptr<OptionImpl>> options_;
+ std::unordered_map<char, size_t> short_names_;
+ std::unordered_map<std::string, size_t> long_names_;
+};
+
+} // namespace
+
+std::unique_ptr<Args> Args::create() {
+ return std::make_unique<ArgsImpl>();
+}
diff --git a/src/args.hh b/src/args.hh
new file mode 100644
index 0000000..b581307
--- /dev/null
+++ b/src/args.hh
@@ -0,0 +1,53 @@
+#ifndef ARGS_HH
+#define ARGS_HH
+
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class Option {
+public:
+ virtual ~Option() = default;
+
+ virtual bool is_set() const = 0;
+ virtual std::string const& arg() const = 0;
+
+protected:
+ Option() = default;
+ Option(Option const&) = delete;
+ Option& operator=(Option const&) = delete;
+};
+
+class Args {
+public:
+ virtual ~Args() = default;
+
+ static std::unique_ptr<Args> create();
+
+ // Returned Option is owned by Args instance.
+ virtual Option const* add_option(
+ char short_name,
+ std::string long_name,
+ std::string description) = 0;
+
+ virtual Option const* add_option_with_arg(
+ char short_name,
+ std::string long_name,
+ std::string description,
+ std::string arg_description) = 0;
+
+ virtual bool run(int argc, char** argv, std::string_view prgname,
+ std::ostream& err, std::vector<std::string>* out) = 0;
+
+ virtual void print_descriptions(std::ostream& out,
+ uint32_t column_width) const = 0;
+
+protected:
+ Args() = default;
+ Args(Args const&) = delete;
+ Args& operator=(Args const&) = delete;
+};
+
+#endif // ARGS_HH
diff --git a/src/common.h b/src/common.h
deleted file mode 100644
index 5b5da04..0000000
--- a/src/common.h
+++ /dev/null
@@ -1,17 +0,0 @@
-#ifndef COMMON_H
-#define COMMON_H
-
-#ifdef HAVE_CONFIG_H
-# include "config.h"
-#endif
-
-#include <assert.h>
-#include <stdbool.h>
-#include <stdlib.h>
-
-#include "compiler.h"
-#include "macros.h"
-
-#define API
-
-#endif /* COMMON_H */
diff --git a/src/common.hh b/src/common.hh
new file mode 100644
index 0000000..1618bc1
--- /dev/null
+++ b/src/common.hh
@@ -0,0 +1,6 @@
+#ifndef COMMON_HH
+#define COMMON_HH
+
+#include <assert.h>
+
+#endif // COMMON_HH
diff --git a/src/compiler.h b/src/compiler.h
deleted file mode 100644
index 6a5ce4a..0000000
--- a/src/compiler.h
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * \file compiler.h
- * Part of common.h, defines macros for hints to the compiler.
- */
-
-#ifndef COMPILER_H
-#define COMPILER_H
-
-#if HAVE___ATTRIBUTE__
-# define MALLOC __attribute__((malloc))
-# define NONULL __attribute__((nonnull))
-# define NONULL_ARGS(...) __attribute__((nonnull (__VA_ARGS__)))
-# define PRINTF(_n,_m) __attribute__((format (printf, (_n), (_m))))
-# define UNUSED __attribute__((unused))
-#else
-# warning Less compile time checks as compiler is unsupported
-/**
- * Mark a function as always returning a new unique pointer or NULL
- */
-# define MALLOC
-/**
- * Mark a function as not taking NULL to any of its pointer parameters
- */
-# define NONULL
-/**
- * Mark a function as not taking NULL to the given list of pointer parameters.
- * The list of parameters is 1 based.
- */
-# define NONULL_ARGS(...)
-/**
- * Mark a function as taking printf format style parameter.
- * @param _n the 1 based index of the format parameter
- * @param _m the 1 based index of the first variable list parameter
- */
-# define PRINTF(_n,_m)
-/**
- * Mark a function paramter as being unused
- */
-# define UNUSED
-#endif
-
-#endif /* COMPILER_H */
diff --git a/src/dynstr.c b/src/dynstr.c
deleted file mode 100644
index 72db26d..0000000
--- a/src/dynstr.c
+++ /dev/null
@@ -1,94 +0,0 @@
-#include "common.h"
-
-#include "dynstr.h"
-
-dynstr_t* dynstr_new(void)
-{
- dynstr_t* str = calloc(1, sizeof(dynstr_t));
- if (str)
- {
- str->size = 64;
- str->data = malloc(str->size);
- if (!str->data)
- {
- free(str);
- return NULL;
- }
- }
- return str;
-}
-
-dynstr_t* dynstr_new_strn(const char* cstr, size_t len)
-{
- dynstr_t* str = calloc(1, sizeof(dynstr_t));
- assert(cstr);
- if (str)
- {
- str->len = len;
- str->size = len + 1;
- str->data = malloc(str->size);
- if (!str->data)
- {
- free(str);
- return NULL;
- }
- memcpy(str->data, cstr, len);
- }
- return str;
-}
-
-void dynstr_free(dynstr_t* str)
-{
- if (str)
- {
- free(str->data);
- free(str);
- }
-}
-
-char* dynstr_done(dynstr_t* str)
-{
- char* ret;
- assert(str);
- assert(str->len < str->size);
- str->data[str->len] = '\0';
- ret = str->data;
- free(str);
- return ret;
-}
-
-const char* dynstr_peek(dynstr_t* str)
-{
- assert(str);
- assert(str->len < str->size);
- str->data[str->len] = '\0';
- return str->data;
-}
-
-bool dynstr_nappend(dynstr_t* str, const char* add, size_t len)
-{
- if (str->len + len >= str->size)
- {
- size_t ns = MAX(str->size * 2, str->len + len + 1);
- char* tmp = realloc(str->data, ns);
- if (!tmp)
- {
- return false;
- }
- str->data = tmp;
- str->size = ns;
- }
- memcpy(str->data + str->len, add, len);
- str->len += len;
- return true;
-}
-
-void dynstr_cut(dynstr_t* str, size_t newlen)
-{
- assert(str);
- assert(newlen <= str->len);
- if (newlen < str->len)
- {
- str->len = newlen;
- }
-}
diff --git a/src/dynstr.h b/src/dynstr.h
deleted file mode 100644
index f18e4f2..0000000
--- a/src/dynstr.h
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * @file dynstr.h
- * Dynamically allocated string.
- */
-
-#ifndef DYNSTR_H
-#define DYNSTR_H
-
-/**
- * Dynamically allocated string.
- * If you modify the content of the struct your self (please don't) make sure
- * len is ALWAYS less than size.
- */
-typedef struct dynstr_t
-{
- /**
- * String data, size large and filled with len bytes of string.
- * Not null-terminated.
- */
- char* data;
- /**
- * Number of bytes in data
- */
- size_t len;
- /**
- * Size of data in bytes
- */
- size_t size;
-} dynstr_t;
-
-#include <string.h>
-
-/**
- * Allocate a dynamically allocated string.
- * Returned string is empty but has allocate at least one byte of space.
- * Returns NULL in case of allocation error.
- * @return empty string
- */
-MALLOC dynstr_t* dynstr_new(void);
-/**
- * Allocate a dynamically allocated string and initialize with parameters.
- * Returned string has at least one more byte allocated than len.
- * Returns NULL in case of allocation error.
- * @param str string to copy len bytes from, may not be NULL
- * @param len number of bytes to copy from str
- * @return string with given data
- */
-MALLOC NONULL dynstr_t* dynstr_new_strn(const char* str, size_t len);
-/**
- * Allocate a dynamically allocated string and initialize with parameter.
- * Returned string has space for at least one more byte.
- * Returns NULL in case of allocation error.
- * @param str null-terminated string to copy from, may not be NULL
- * @return string with given data
- */
-static inline MALLOC NONULL dynstr_t* dynstr_new_str(const char* str)
-{
- assert(str);
- return dynstr_new_strn(str, strlen(str));
-}
-/**
- * Free a dynamic string.
- * @param str string to free, may be NULL
- */
-void dynstr_free(dynstr_t* str);
-
-/**
- * Return null-terminated static string and free dynamic string.
- * @param str string to free, may not be NULL
- * @return null-terminated content of str, never NULL
- */
-NONULL char* dynstr_done(dynstr_t* str);
-
-/**
- * Return null-terminated static string.
- * The returned string is only valid until the next call to a dynstr method
- * on the same string.
- * @param str string to free, may not be NULL
- * @return null-terminated content of str, never NULL
- */
-NONULL const char* dynstr_peek(dynstr_t* str);
-
-/**
- * Append len bytes of data from add to dynamic string.
- * @param str string to append to, may not be NULL
- * @param add string to append, may not be NULL
- * @param len number of bytes to append from add
- * @return false in case of allocation error
- */
-NONULL bool dynstr_nappend(dynstr_t* str, const char* add, size_t len);
-
-/**
- * Append string to dynamic string.
- * @param str string to append to, may not be NULL
- * @param add null-terminated string to append, may not be NULL
- * @return false in case of allocation error
- */
-static inline NONULL bool dynstr_append(dynstr_t* str, const char* add)
-{
- assert(add);
- return dynstr_nappend(str, add, strlen(add));
-}
-
-/**
- * Append character to dynamic string.
- * @param str string to append to, may not be NULL
- * @param c character to append
- * @return false in case of allocation error
- */
-static inline NONULL bool dynstr_appendc(dynstr_t* str, char c)
-{
- return dynstr_nappend(str, &c, 1);
-}
-
-/**
- * Make dynamic string shorter by cutting of a bit at the end.
- * @param str string to cut shorter, may not be NULL
- * @param newlen new length of string
- */
-NONULL void dynstr_cut(dynstr_t* str, size_t newlen);
-
-#endif /* DYNSTR_H */
diff --git a/src/io.cc b/src/io.cc
new file mode 100644
index 0000000..0fb31d9
--- /dev/null
+++ b/src/io.cc
@@ -0,0 +1,41 @@
+#include "common.hh"
+
+#include "io.hh"
+
+#include <errno.h>
+#include <unistd.h>
+
+namespace io {
+
+bool read_all(int fd, std::string* out) {
+ char buf[32768];
+ while (true) {
+ auto ret = read(fd, buf, sizeof(buf));
+ if (ret == 0)
+ return true;
+ if (ret < 0) {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ out->append(buf, ret);
+ }
+}
+
+bool write_all(int fd, std::string const& in) {
+ size_t offset = 0;
+ while (offset < in.size()) {
+ auto ret = write(fd, in.data() + offset, in.size() - offset);
+ if (ret == 0)
+ return false;
+ if (ret < 0) {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ offset += ret;
+ }
+ return true;
+}
+
+} // namespace io
diff --git a/src/io.hh b/src/io.hh
new file mode 100644
index 0000000..8786549
--- /dev/null
+++ b/src/io.hh
@@ -0,0 +1,13 @@
+#ifndef IO_HH
+#define IO_HH
+
+#include <string>
+
+namespace io {
+
+bool read_all(int fd, std::string* out);
+bool write_all(int fd, std::string const& in);
+
+} // namespace io
+
+#endif // IO_HH
diff --git a/src/macros.h b/src/macros.h
deleted file mode 100644
index 06d59c6..0000000
--- a/src/macros.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * \file macros.h
- * Part of common.h, defines common small macros if needed
- */
-
-#ifndef MACROS_H
-#define MACROS_H
-
-#ifndef MIN
-/** x < y ? x : y */
-# define MIN(_x, _y) (((_x) < (_y)) ? (_x) : (_y))
-#endif
-
-#ifndef MAX
-/** x > y ? x : y */
-# define MAX(_x, _y) (((_x) > (_y)) ? (_x) : (_y))
-#endif
-
-#endif /* MACROS_H */
diff --git a/src/main.c b/src/main.c
deleted file mode 100644
index 9bb5b6c..0000000
--- a/src/main.c
+++ /dev/null
@@ -1,896 +0,0 @@
-#include "common.h"
-
-#include <errno.h>
-#include <math.h>
-#include <stdio.h>
-#include <string.h>
-
-#include <xcb/xcb.h>
-#include <xcb/xcb_event.h>
-#include <xcb/xcb_keysyms.h>
-#if HAVE_XCB_ICCCM
-# include <xcb/xcb_icccm.h>
-#endif
-
-#define HAVE_STRUCT_TIMESPEC 1
-
-#include "paths.h"
-#include "safe_fifo.h"
-#include "thread.h"
-
-#if HAVE_XCB_ICCCM
-typedef struct atoms_t
-{
- xcb_atom_t WM_DELETE_WINDOW;
- xcb_atom_t WM_PROTOCOLS;
-} atoms_t;
-#endif
-
-typedef enum message_type_t
-{
- MSG_QUIT,
- MSG_DRAW,
- MSG_SWAP,
- MSG_CLEAR
-} message_type_t;
-
-typedef struct message_t
-{
- message_type_t type;
-} message_t;
-
-static message_t quit_message = { MSG_QUIT };
-static message_t draw_message = { MSG_DRAW };
-static message_t swap_message = { MSG_SWAP };
-static message_t clear_message = { MSG_CLEAR };
-
-typedef struct time_target_t time_target_t;
-
-typedef bool (* timer_callback_t)(void *data);
-
-struct time_target_t
-{
- time_target_t *next;
- struct timespec target;
- unsigned long interval_ms;
- void *data;
- timer_callback_t callback;
-};
-
-typedef struct main_t
-{
- xcb_connection_t *conn;
- xcb_window_t wnd;
- xcb_gcontext_t gc;
- xcb_font_t font;
-#if HAVE_XCB_ICCCM
- atoms_t atoms;
-#endif
- safe_fifo_t *queue;
- const char *state;
- paths_t *paths;
- time_target_t *first_timer;
- bool save_queued;
- bool redraw_queued;
- unsigned int width, height;
- uint32_t background[2], foreground[2];
-
- /* State */
- bool working;
- time_t last_time;
- unsigned long total_min;
-} main_t;
-
-static const char DEFAULT_STATE[] = "timer.state";
-
-static const char FONT_NAME[] = "7x13";
-
-static const unsigned long REDRAW_INTERVAL_MS = 36 * 1000;
-
-static bool handle_arguments(main_t *m, int argc, char **argv, int *exitcode);
-static void draw(main_t *m);
-static void *run_xcb(void *arg);
-static bool load_state(main_t *m);
-static bool save_state(main_t *m);
-static void default_state(main_t *m);
-static void swap(main_t *m);
-static void clear(main_t *m);
-static void queue_save(main_t *m);
-static void add_timer(main_t *m, unsigned long interval_ms,
- timer_callback_t callback, void *data);
-static void insert_timer(main_t *m, time_target_t *target);
-static void run_timer(main_t *m);
-static bool redraw(void *arg);
-static int timeval_cmp(const struct timespec* x, const struct timespec* y);
-
-int main(int argc, char **argv)
-{
- main_t m;
- xcb_screen_iterator_t iter;
- xcb_screen_t *screen;
- int screen_index;
- uint32_t mask, values[2];
- xcb_void_cookie_t cookie[10];
- size_t cookies = 0;
- int exitcode;
- bool quit = false;
- memset(&m, 0, sizeof(m));
-
- if (!handle_arguments(&m, argc, argv, &exitcode))
- {
- return exitcode;
- }
-
- m.paths = paths_new();
- if (!m.paths)
- {
- fputs("Unable to allocate memory.\n", stderr);
- return EXIT_FAILURE;
- }
-
- if (!load_state(&m))
- {
- fputs("Error loading old state.\n", stderr);
- paths_unref(m.paths);
- return EXIT_FAILURE;
- }
-
- m.conn = xcb_connect(NULL, &screen_index);
- if (!m.conn)
- {
- fputs("Unable to connect to X11 display.\n", stderr);
- paths_unref(m.paths);
- return EXIT_FAILURE;
- }
-
-#if HAVE_XCB_ICCCM
- {
- xcb_intern_atom_cookie_t cookie[2];
- xcb_intern_atom_reply_t *reply;
- xcb_generic_error_t *err;
-
- cookie[0] = xcb_intern_atom(m.conn, 0, strlen("WM_DELETE_WINDOW"),
- "WM_DELETE_WINDOW");;
- cookie[1] = xcb_intern_atom(m.conn, 0, strlen("WM_PROTOCOLS"),
- "WM_PROTOCOLS");;
- reply = xcb_intern_atom_reply(m.conn, cookie[0], &err);
- if (!reply)
- {
- fprintf(stderr, "ICCCM init atoms failed\n");
- xcb_disconnect(m.conn);
- paths_unref(m.paths);
- return EXIT_FAILURE;
- }
- m.atoms.WM_DELETE_WINDOW = reply->atom;
- free(reply);
- reply = xcb_intern_atom_reply(m.conn, cookie[1], &err);
- if (!reply)
- {
- fprintf(stderr, "ICCCM init atoms failed\n");
- xcb_disconnect(m.conn);
- paths_unref(m.paths);
- return EXIT_FAILURE;
- }
- m.atoms.WM_PROTOCOLS = reply->atom;
- free(reply);
- }
-#endif
-
- iter = xcb_setup_roots_iterator(xcb_get_setup(m.conn));
- while (screen_index-- > 0)
- {
- xcb_screen_next(&iter);
- }
-
- screen = iter.data;
-
- {
- xcb_alloc_color_cookie_t req[4];
- xcb_alloc_color_reply_t* rep;
- /* background, not working */
- req[0] = xcb_alloc_color(m.conn, screen->default_colormap,
- 0x8000, 0x8000, 0x8000);
- /* background, working */
- req[1] = xcb_alloc_color(m.conn, screen->default_colormap,
- 0x8000, 0xffff, 0x8000);
- /* foreground, not working */
- req[2] = xcb_alloc_color(m.conn, screen->default_colormap,
- 0, 0, 0);
- /* foreground, working */
- req[3] = xcb_alloc_color(m.conn, screen->default_colormap,
- 0, 0, 0);
- rep = xcb_alloc_color_reply(m.conn, req[0], 0);
- m.background[0] = rep ? rep->pixel : screen->white_pixel;
- free(rep);
- rep = xcb_alloc_color_reply(m.conn, req[1], 0);
- m.background[1] = rep ? rep->pixel : screen->white_pixel;
- free(rep);
- rep = xcb_alloc_color_reply(m.conn, req[2], 0);
- m.foreground[0] = rep ? rep->pixel : screen->black_pixel;
- free(rep);
- rep = xcb_alloc_color_reply(m.conn, req[3], 0);
- m.foreground[1] = rep ? rep->pixel : screen->black_pixel;
- free(rep);
- }
-
- m.font = xcb_generate_id(m.conn);
- cookie[cookies++] = xcb_open_font_checked(m.conn, m.font,
- sizeof(FONT_NAME) /
- sizeof(FONT_NAME[0]),
- FONT_NAME);
-
- mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
- values[0] = screen->black_pixel;
- values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS |
- XCB_EVENT_MASK_KEY_RELEASE;
-
- m.width = 100;
- m.height = 50;
-
- m.wnd = xcb_generate_id(m.conn);
- cookie[cookies++] = xcb_create_window_checked(m.conn, XCB_COPY_FROM_PARENT,
- m.wnd, screen->root,
- 0, 0, m.width, m.height, 0,
- XCB_WINDOW_CLASS_INPUT_OUTPUT,
- screen->root_visual,
- mask, values);
-
- m.gc = xcb_generate_id(m.conn);
- mask = XCB_GC_FONT;
- values[0] = m.font;
- cookie[cookies++] = xcb_create_gc_checked(m.conn, m.gc, m.wnd,
- mask, values);
-
- cookie[cookies++] = xcb_map_window_checked(m.conn, m.wnd);
-
-#if HAVE_XCB_ICCCM
- {
- xcb_atom_t protocols[1];
- protocols[0] = m.atoms.WM_DELETE_WINDOW;
-
- xcb_icccm_set_wm_protocols(m.conn, m.wnd, m.atoms.WM_PROTOCOLS,
- 1, protocols);
- xcb_icccm_set_wm_name(m.conn, m.wnd, XCB_ATOM_STRING, 8, 5, "timer");
- xcb_icccm_set_wm_class(m.conn, m.wnd, 8, "jk.timer");
- }
-#endif
-
- while (cookies--)
- {
- xcb_generic_error_t *error = xcb_request_check(m.conn, cookie[cookies]);
- if (error)
- {
- fprintf(stderr, "Error: %d\n", error->error_code);
- xcb_disconnect(m.conn);
- paths_unref(m.paths);
- return EXIT_FAILURE;
- }
- }
-
- xcb_flush(m.conn);
-
- m.queue = safe_fifo_new();
-
- thread_new(run_xcb, &m);
-
- if (m.working)
- {
- add_timer(&m, REDRAW_INTERVAL_MS, redraw, &m);
- m.redraw_queued = true;
- }
-
- while (!quit)
- {
- message_t *msg;
- if (m.first_timer)
- {
- struct timespec now;
- thread_abstime(&now, 0);
- if (timeval_cmp(&now, &m.first_timer->target) >= 0)
- {
- run_timer(&m);
- msg = safe_fifo_trypop(m.queue);
- }
- else
- {
- msg = safe_fifo_timedpop(m.queue, &m.first_timer->target);
- }
- }
- else
- {
- msg = safe_fifo_pop(m.queue);
- }
- if (msg)
- {
- switch (msg->type)
- {
- case MSG_QUIT:
- quit = true;
- break;
- case MSG_DRAW:
- draw(&m);
- break;
- case MSG_SWAP:
- swap(&m);
- break;
- case MSG_CLEAR:
- clear(&m);
- break;
- }
- }
- }
-
- if (m.save_queued)
- {
- save_state(&m);
- }
-
- xcb_close_font(m.conn, m.font);
- xcb_free_gc(m.conn, m.gc);
- xcb_destroy_window(m.conn, m.wnd);
-
- xcb_disconnect(m.conn);
- while (m.first_timer)
- {
- time_target_t *next = m.first_timer->next;
- free(m.first_timer);
- m.first_timer = next;
- }
- paths_unref(m.paths);
- safe_fifo_unref(m.queue);
-
- return EXIT_SUCCESS;
-}
-
-static void print_usage(void)
-{
- fputs("Usage: timer [OPTION]...\n", stdout);
- fputs("Timer is a timekeeping tool.\n", stdout);
- fputs("\n", stdout);
- fputs("Options:\n", stdout);
- fputs(" -state FILE load state from FILE instead of default\n", stdout);
- fputs(" -help display this information and exit\n", stdout);
- fputs(" -version display version and exit\n", stdout);
-}
-
-static void print_version(void)
-{
- fputs("timer version " VERSION " written by Joel Klinghed\n", stdout);
-}
-
-bool handle_arguments(main_t *m, int argc, char **argv, int *exitcode)
-{
- bool usage = false, version = false, error = false;
- int a;
- for (a = 1; a < argc; a++)
- {
- if (argv[a][0] == '-')
- {
- if (strcmp(argv[a] + 1, "help") == 0)
- {
- usage = true;
- break;
- }
- else if (strcmp(argv[a] + 1, "version") == 0)
- {
- version = true;
- break;
- }
- else if (strcmp(argv[a] + 1, "state") == 0)
- {
- if (++a == argc)
- {
- error = true;
- fputs("Option `state` expects an argument.\n", stderr);
- break;
- }
- if (m->state)
- {
- error = true;
- fputs("Option `state` given twice.\n", stderr);
- break;
- }
- m->state = argv[a];
- }
- else
- {
- error = true;
- fprintf(stderr, "Unknown option: %s\n", argv[a] + 1);
- break;
- }
- }
- else
- {
- break;
- }
- }
- if (!error && a < argc)
- {
- fputs("Too many arguments given.\n", stderr);
- error = true;
- }
- if (usage)
- {
- print_usage();
- *exitcode = error ? EXIT_FAILURE : EXIT_SUCCESS;
- return false;
- }
- if (error)
- {
- fputs("Try `timer -help` for usage.\n", stderr);
- *exitcode = EXIT_FAILURE;
- return false;
- }
- if (version)
- {
- print_version();
- *exitcode = EXIT_SUCCESS;
- return false;
- }
- return true;
-}
-
-void draw(main_t *m)
-{
- xcb_rectangle_t r;
- xcb_char2b_t *tmp;
- uint32_t values[2];
- char text[50];
- int x, y, i, len;
- xcb_query_text_extents_cookie_t cookie;
- xcb_query_text_extents_reply_t *reply;
-
- if (m->working)
- {
- double diff = difftime(time(NULL), m->last_time);
- diff /= 60.0 * 60.0;
-
- len = snprintf(text, sizeof(text), "%.2f (%.2f)", diff,
- (double)m->total_min / 60.0 + diff);
- }
- else
- {
- len = snprintf(text, sizeof(text), "(%.2f)",
- (double)m->total_min / 60.0);
- }
- if (len < 0 || len == sizeof(text))
- {
- return;
- }
- tmp = malloc(len * sizeof(xcb_char2b_t));
- for (i = 0; i < len; i++)
- {
- tmp[i].byte1 = 0;
- tmp[i].byte2 = text[i];
- }
-
- cookie = xcb_query_text_extents(m->conn, m->font, len, tmp);
-
- r.x = 0;
- r.y = 0;
- r.width = m->width;
- r.height = m->height;
-
- values[0] = m->background[m->working ? 1 : 0];
- xcb_change_gc(m->conn, m->gc, XCB_GC_FOREGROUND, values);
- xcb_poly_fill_rectangle(m->conn, m->wnd, m->gc, 1, &r);
-
- values[0] = m->foreground[m->working ? 1 : 0];
- values[1] = m->background[m->working ? 1 : 0];
- xcb_change_gc(m->conn, m->gc, XCB_GC_FOREGROUND | XCB_GC_BACKGROUND, values);
-
- x = 0;
- y = m->height / 2;
-
- reply = xcb_query_text_extents_reply(m->conn, cookie, NULL);
- if (reply)
- {
- if (reply->overall_width < m->width)
- {
- x = (m->width - reply->overall_width) / 2;
- }
- else
- {
- x = 0;
- }
- y += reply->font_ascent / 2;
- free(reply);
- }
-
- xcb_image_text_16(m->conn, len, m->wnd, m->gc, x, y, tmp);
-
- xcb_flush(m->conn);
-
- free(tmp);
-}
-
-static void send_message(main_t *m, message_t* msg)
-{
- safe_fifo_push(m->queue, msg);
-}
-
-void *run_xcb(void *arg)
-{
- main_t *m = arg;
- xcb_key_symbols_t *syms;
-
- syms = xcb_key_symbols_alloc(m->conn);
-
- for (;;)
- {
- bool done = false;
- xcb_generic_event_t *event = xcb_wait_for_event(m->conn);
- if (!event || xcb_connection_has_error(m->conn))
- {
- free(event);
- break;
- }
- switch (XCB_EVENT_RESPONSE_TYPE(event))
- {
- case XCB_EXPOSE:
- {
- xcb_expose_event_t *e = (xcb_expose_event_t*)event;
- if (e->count == 0)
- {
- send_message(m, &draw_message);
- }
- break;
- }
-#if HAVE_XCB_ICCCM
- case XCB_CLIENT_MESSAGE:
- {
- xcb_client_message_event_t *e = (xcb_client_message_event_t*)event;
- if (e->type == m->atoms.WM_PROTOCOLS && e->format == 32 &&
- e->data.data32[0] == m->atoms.WM_DELETE_WINDOW)
- {
- done = true;
- }
- break;
- }
-#endif
- case XCB_BUTTON_PRESS:
- {
- xcb_button_press_event_t *e = (xcb_button_press_event_t*)event;
- if (e->detail == 1)
- {
- send_message(m, &swap_message);
- }
- break;
- }
- case XCB_KEY_RELEASE:
- {
- xcb_key_press_event_t *e = (xcb_key_press_event_t*)event;
- xcb_keysym_t sym = xcb_key_press_lookup_keysym(syms, e, 0);
- if (sym == ' ')
- {
- send_message(m, &swap_message);
- }
- else if (sym == 'r' && (e->state & XCB_MOD_MASK_CONTROL))
- {
- send_message(m, &clear_message);
- }
- break;
- }
- case XCB_MAPPING_NOTIFY:
- {
- xcb_mapping_notify_event_t *e = (xcb_mapping_notify_event_t*)event;
- xcb_refresh_keyboard_mapping(syms, e);
- break;
- }
- }
- free(event);
- if (done)
- {
- break;
- }
- }
- xcb_key_symbols_free(syms);
- send_message(m, &quit_message);
- return NULL;
-}
-
-bool load_state(main_t *m)
-{
- FILE *fh;
- char *tmp, *line;
- const char *fname;
- char buf[1024];
- char *end;
- long l;
- unsigned long ul;
- struct tm tm;
- if (m->state)
- {
- fname = m->state;
- tmp = NULL;
- }
- else
- {
- const char *dir = paths_user_dir(m->paths, PATHS_DATA);
- size_t dlen = strlen(dir);
- tmp = malloc(dlen + 1 + sizeof(DEFAULT_STATE));
- if (!tmp)
- {
- fputs("Out of memory\n", stderr);
- return false;
- }
- memcpy(tmp, dir, dlen);
- tmp[dlen] = '/';
- memcpy(tmp + dlen + 1, DEFAULT_STATE, sizeof(DEFAULT_STATE));
- fname = tmp;
- }
- fh = fopen(fname, "rb");
- if (!fh)
- {
- if (errno == ENOENT)
- {
- default_state(m);
- free(tmp);
- return true;
- }
- fprintf(stderr, "Error loading state from %s: %s\n", fname,
- strerror(errno));
- free(tmp);
- return false;
- }
-
- line = fgets(buf, sizeof(buf), fh);
- if (!line)
- {
- fprintf(stderr, "Error reading state from %s: %s\n", fname,
- strerror(errno));
- free(tmp);
- return false;
- }
-
- fclose(fh);
-
- end = NULL;
- l = strtol(line, &end, 10);
- if ((l != -1 && l != 1) || !end || *end != '|')
- {
- fprintf(stderr, "Invalid data in state %s: %s\n", fname,
- strerror(errno));
- free(tmp);
- return false;
- }
-
- line = end + 1;
- end = NULL;
- ul = strtoul(line, &end, 10);
- if (!end || *end != '|')
- {
- fprintf(stderr, "Invalid data in state %s: %s\n", fname,
- strerror(errno));
- free(tmp);
- return false;
- }
-
- end = strptime(end + 1, "%Y-%m-%d %H:%M:%S", &tm);
- if (!end)
- {
- fprintf(stderr, "Invalid data in state %s: %s\n", fname,
- strerror(errno));
- free(tmp);
- return false;
- }
-
- m->working = l == 1;
- m->last_time = timegm(&tm);
- m->total_min = ul;
-
- free(tmp);
- return true;
-}
-
-bool save_state(main_t *m)
-{
- paths_file_t *file = paths_write(m->paths, PATHS_DATA,
- m->state ? m->state : DEFAULT_STATE,
- PATHS_CREATE | PATHS_TRUNCATE,
- 0600);
- char buf[1024];
- size_t len2;
- int pos, len;
- m->save_queued = false;
-
- if (!file)
- {
- fprintf(stderr, "Failed to save state: %s\n", strerror(errno));
- return false;
- }
-
- len = snprintf(buf, sizeof(buf), "%d|%lu|",
- m->working ? 1 : -1, m->total_min);
- if (len < 0 || len == sizeof(buf))
- {
- fprintf(stderr, "Failed to save state: %s\n", strerror(errno));
- paths_file_abort(file);
- return false;
- }
- len2 = strftime(buf + len, sizeof(buf) - len, "%Y-%m-%d %H:%M:%S",
- gmtime(&m->last_time));
- if (len2 == 0 || len2 == sizeof(buf) - len)
- {
- fprintf(stderr, "Failed to save state: %s\n", strerror(errno));
- paths_file_abort(file);
- return false;
- }
- len += len2;
- pos = 0;
- while (pos < len)
- {
- ssize_t got = paths_file_write(file, buf + pos, len - pos);
- if (got <= 0)
- {
- fprintf(stderr, "Failed to save state: %s\n", strerror(errno));
- paths_file_abort(file);
- return false;
- }
- pos += got;
- }
-
- if (paths_file_close(file))
- {
- fprintf(stderr, "Failed to finish saving state: %s\n", strerror(errno));
- paths_file_abort(file);
- return false;
- }
- return true;
-}
-
-void default_state(main_t *m)
-{
- m->working = false;
- m->last_time = time(NULL);
- m->total_min = 0;
-}
-
-void swap(main_t *m)
-{
- if (m->working)
- {
- double diff = difftime(time(NULL), m->last_time);
- m->total_min += round(diff / 60.0);
- m->working = false;
- }
- else
- {
- m->working = true;
- if (!m->redraw_queued)
- {
- add_timer(m, REDRAW_INTERVAL_MS, redraw, m);
- m->redraw_queued = true;
- }
- }
-
- m->last_time = time(NULL);
-
- draw(m);
- queue_save(m);
-}
-
-void clear(main_t *m)
-{
- if (m->working)
- {
- return;
- }
-
- default_state(m);
- draw(m);
- queue_save(m);
-}
-
-static bool do_save(void *arg)
-{
- main_t *m = arg;
- assert(m->save_queued);
- save_state(m);
- return false;
-}
-
-void queue_save(main_t *m)
-{
- if (m->save_queued)
- {
- return;
- }
- add_timer(m, 5000, do_save, m);
- m->save_queued = true;
-}
-
-void add_timer(main_t *m, unsigned long interval_ms,
- timer_callback_t callback, void *data)
-{
- time_target_t* tgt = calloc(1, sizeof(time_target_t));
- assert(interval_ms > 0);
- tgt->interval_ms = interval_ms;
- tgt->callback = callback;
- tgt->data = data;
- thread_abstime(&tgt->target, interval_ms);
- insert_timer(m, tgt);
-}
-
-void insert_timer(main_t* m, time_target_t *target)
-{
- time_target_t *cur, *last;
- assert(target->next == NULL);
- if (!m->first_timer)
- {
- m->first_timer = target;
- return;
- }
- last = NULL;
- cur = m->first_timer;
- while (cur)
- {
- if (timeval_cmp(&cur->target, &target->target) > 0)
- {
- target->next = cur;
- if (last == NULL)
- {
- m->first_timer = target;
- return;
- }
- else
- {
- break;
- }
- }
- last = cur;
- cur = last->next;
- }
- last->next = target;
-}
-
-void run_timer(main_t *m)
-{
- time_target_t *target = m->first_timer;
- m->first_timer = target->next;
- target->next = NULL;
- if (target->callback(target->data))
- {
- thread_abstime(&target->target, target->interval_ms);
- insert_timer(m, target);
- }
- else
- {
- free(target);
- }
-}
-
-bool redraw(void *arg)
-{
- main_t* m = arg;
- assert(m->redraw_queued);
- if (m->working)
- {
- draw(m);
- return true;
- }
- m->redraw_queued = false;
- return false;
-}
-
-int timeval_cmp(const struct timespec* x, const struct timespec* y)
-{
- assert(x && y);
- if (x->tv_sec < y->tv_sec)
- {
- return -1;
- }
- else if (x->tv_sec == y->tv_sec)
- {
- if (x->tv_nsec < y->tv_nsec)
- {
- return -1;
- }
- else if (x->tv_nsec == y->tv_nsec)
- {
- return 0;
- }
- }
-
- return 1;
-}
diff --git a/src/paths.c b/src/paths.c
deleted file mode 100644
index 1bdc4e3..0000000
--- a/src/paths.c
+++ /dev/null
@@ -1,1029 +0,0 @@
-#include "common.h"
-
-#include "paths.h"
-#include "ref.h"
-#include "thread.h"
-#include "safe_fifo.h"
-#include "strutil.h"
-
-#include <sys/file.h>
-#include <sys/stat.h>
-#include <dirent.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <glob.h>
-#include <pwd.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-
-#define SEARCH_CACHE_SIZE (2)
-
-typedef struct search_t
-{
- const char* dir;
- const char** dirs;
- char* match;
- const char* match_name;
- bool plain;
- paths_find_callback_t find_callback;
- void* userdata;
-} search_t;
-
-typedef struct searchargs_t
-{
- bool quit;
- safe_fifo_t* queue;
- thread_lock_t* lock;
- search_t* cache[SEARCH_CACHE_SIZE];
-} searchargs_t;
-
-struct paths_t
-{
- REF_DECLARE();
- char* user_data;
- char* user_config;
- char* user_cache;
- char* user_runtime;
- char** search_data;
- char** search_config;
-
- searchargs_t* search;
-#ifdef DEBUG
- thread_t* searcher;
-#endif
-};
-
-static void free_list(char** list);
-static void free_search_data(search_t* search);
-static bool paths_init(paths_t* paths);
-static char* build_path(paths_t* paths, paths_source_t source, ...);
-static bool create_dir(paths_t* paths, paths_source_t source,
- const char* addon, size_t addon_len);
-static void* searcher(void* _args);
-static search_t* get_search(searchargs_t* args);
-static void put_search(searchargs_t* args, search_t* search);
-
-paths_t* paths_new(void)
-{
- paths_t* paths = calloc(1, sizeof(paths_t));
- if (!paths)
- {
- return NULL;
- }
- if (!REF_INIT(paths))
- {
- free(paths);
- return NULL;
- }
- for (;;)
- {
- paths->search = calloc(1, sizeof(searchargs_t));
- if (!paths->search)
- {
- break;
- }
- paths->search->lock = thread_lock_new();
- if (!paths->search->lock)
- {
- break;
- }
- paths->search->queue = safe_fifo_new();
- if (!paths->search->queue)
- {
- break;
- }
- safe_fifo_ref(paths->search->queue);
-#ifdef DEBUG
- paths->searcher = thread_joinable_new(searcher, paths->search);
- if (!paths->searcher)
- {
- safe_fifo_unref(paths->search->queue);
- break;
- }
-#else
- if (!thread_new(searcher, paths->search))
- {
- safe_fifo_unref(paths->search->queue);
- break;
- }
-#endif
- if (!paths_init(paths))
- {
- break;
- }
- return paths;
- }
- paths_unref(paths);
- return NULL;
-}
-
-void paths_ref(paths_t* paths)
-{
- assert(paths);
- REF_INC(paths);
-}
-
-void paths_unref(paths_t* paths)
-{
- if (paths && REF_DEC(paths))
- {
- if (paths->search)
- {
- if (paths->search->queue)
- {
- search_t* search = get_search(paths->search);
- paths->search->quit = true;
- if (search)
- {
- memset(search, 0, sizeof(search_t));
- safe_fifo_push(paths->search->queue, search);
- }
- safe_fifo_unref(paths->search->queue);
- }
-#ifdef DEBUG
- thread_join(paths->searcher);
-#endif
- }
- free(paths->user_data);
- free(paths->user_config);
- free(paths->user_cache);
- free(paths->user_runtime);
- free_list(paths->search_data);
- free_list(paths->search_config);
- REF_FREE(paths);
- free(paths);
- }
-}
-
-const char* paths_user_dir(paths_t* paths, paths_source_t source)
-{
- assert(paths);
- switch (source)
- {
- case PATHS_DATA:
- return paths->user_data;
- case PATHS_CONFIG:
- return paths->user_config;
- case PATHS_CACHE:
- return paths->user_cache;
- case PATHS_RUNTIME:
- return paths->user_runtime;
- }
- assert(false);
- return NULL;
-}
-
-const char** paths_search_dirs(paths_t* paths, paths_source_t source)
-{
- assert(paths);
- switch (source)
- {
- case PATHS_DATA:
- return (const char**)paths->search_data;
- case PATHS_CONFIG:
- return (const char**)paths->search_config;
- case PATHS_CACHE:
- case PATHS_RUNTIME:
- return NULL;
- }
- assert(false);
- return NULL;
-}
-
-struct paths_file_t
-{
- paths_source_t source;
- char* path, * target;
- int fd;
-};
-
-paths_file_t* paths_write(paths_t* paths, paths_source_t source,
- const char* filename, unsigned int flags, ...)
-{
- paths_file_t* file;
- int f;
- const char* subpath;
- assert(filename);
- if (source == PATHS_RUNTIME && !paths->user_runtime)
- {
- errno = ENOENT;
- return NULL;
- }
- if (filename[0] == '/')
- {
- filename++;
- }
- subpath = strrchr(filename, '/');
- if (subpath)
- {
- if (!create_dir(paths, source, filename, subpath - filename))
- {
- return NULL;
- }
- }
- else
- {
- if (!create_dir(paths, source, NULL, 0))
- {
- return NULL;
- }
- }
- file = calloc(1, sizeof(paths_file_t));
- if (!file)
- {
- return NULL;
- }
- file->source = source;
- if (source == PATHS_CONFIG &&
- (((flags & PATHS_CREATE) && (flags & PATHS_TRUNCATE)) ||
- ((flags & PATHS_CREATE) && (flags & PATHS_EXCLUSIVE))))
- {
- file->target = build_path(paths, source, filename, NULL);
- if (!file->target)
- {
- free(file);
- return NULL;
- }
- file->path = build_path(paths, source, ".#", filename, NULL);
- }
- else
- {
- file->path = build_path(paths, source, filename, NULL);
- }
- if (!file->path)
- {
- free(file->target);
- free(file);
- return NULL;
- }
- f = O_WRONLY;
- if ((flags & PATHS_APPEND))
- {
- f |= O_APPEND;
- }
- if ((flags & PATHS_CREATE))
- {
- f |= O_CREAT;
- if ((flags & PATHS_EXCLUSIVE))
- {
- f |= O_EXCL;
- }
- }
- if ((flags & PATHS_TRUNCATE))
- {
- f |= O_TRUNC;
- }
-#ifdef O_EXLOCK
- f |= O_EXLOCK;
-#endif
- if ((f & O_CREAT))
- {
- va_list args;
- va_start(args, flags);
- file->fd = open(file->path, f, va_arg(args, int));
- va_end(args);
- }
- else
- {
- file->fd = open(file->path, f);
- }
- if (file->fd < 0)
- {
- free(file->path);
- free(file->target);
- free(file);
- return NULL;
- }
-#ifndef O_EXLOCK
- for (;;)
- {
- if (flock(file->fd, LOCK_EX) == 0)
- {
- break;
- }
- if (errno != EINTR)
- {
- if ((flags & O_CREAT))
- {
- unlink(file->path);
- }
- close(file->fd);
- free(file->path);
- free(file->target);
- free(file);
- return NULL;
- }
- }
-#endif
- return file;
-}
-
-ssize_t paths_file_write(paths_file_t* file,
- const void* data, size_t size)
-{
- size_t pos = 0;
- assert(file);
- assert(size == 0 || data);
- while (pos < size)
- {
- ssize_t ret = write(file->fd, data + pos, size - pos);
- if (ret < 0)
- {
- if (errno == EAGAIN || errno == EWOULDBLOCK ||
- errno == EINTR)
- {
- /* Non fatal errors */
- if (pos > 0)
- {
- return pos;
- }
- }
- return -1;
- }
- if (ret == 0)
- {
- return pos;
- }
- pos += ret;
- }
- return size;
-}
-
-int paths_file_close(paths_file_t* file)
-{
- assert(file);
- if (close(file->fd))
- {
- return -1;
- }
- file->fd = -1;
- if (file->target)
- {
- size_t len = strlen(file->target);
- char* tmp = malloc(len + 2);
- int ret;
- if (!tmp)
- {
- return -1;
- }
- memcpy(tmp, file->target, len);
- memcpy(tmp + len, "~", 2);
- ret = rename(file->target, tmp);
- free(tmp);
- if (ret && errno == ENOENT)
- {
- int olderrno = errno;
- /* Check if rename failed because target doesn't exist */
- if (access(file->target, F_OK) == 0 ||
- errno != ENOENT)
- {
- /* Unable to create backup */
- errno = olderrno;
- return -1;
- }
- }
- if (rename(file->path, file->target))
- {
- return -1;
- }
- free(file->target);
- }
- free(file->path);
- free(file);
- return 0;
-}
-
-void paths_file_abort(paths_file_t* file)
-{
- if (file)
- {
- if (file->fd >= 0)
- {
- close(file->fd);
- }
- unlink(file->path);
- free(file->path);
- free(file->target);
- free(file);
- }
-}
-
-bool create_dir(paths_t* paths, paths_source_t source, const char* addon,
- size_t addon_len)
-{
- const char* dir = paths_user_dir(paths, source);
- char* tmp = NULL;
- assert(dir);
- assert(addon || !addon_len);
- if (addon_len)
- {
- size_t dlen = strlen(dir);
- tmp = malloc(dlen + 1 + addon_len + 1);
- if (!tmp)
- {
- return false;
- }
- memcpy(tmp, dir, dlen);
- tmp[dlen++] = '/';
- memcpy(tmp + dlen, addon, addon_len);
- tmp[dlen + addon_len] = '\0';
- dir = tmp;
- }
- if (mkdir(dir, 0700))
- {
- free(tmp);
- return errno == EEXIST;
- }
- free(tmp);
- return true;
-}
-
-void free_list(char** list)
-{
- if (list)
- {
- char** i;
- for (i = list; *i; i++)
- {
- free(*i);
- }
- free(list);
- }
-}
-
-typedef struct homedir_t
-{
- char* path;
- size_t len;
-} homedir_t;
-
-static char* get_userdir(const char* envvar, homedir_t* homedir,
- const char* addon);
-static char** get_searchdirs(const char* envvar, const char* default_list);
-
-static bool valid_runtime(const char* path);
-
-bool paths_init(paths_t* paths)
-{
- homedir_t homedir = { NULL, 0 };
- paths->user_data = get_userdir("XDG_DATA_HOME", &homedir, "/.local/share");
- paths->user_config = get_userdir("XDG_CONFIG_HOME", &homedir, "/.config");
- paths->user_cache = get_userdir("XDG_CACHE_HOME", &homedir, "/.cache");
- paths->user_runtime = get_userdir("XDG_RUNTIME_HOME", NULL, NULL);
- free(homedir.path);
- if (!paths->user_data || !paths->user_config || !paths->user_cache)
- {
- return false;
- }
- if (paths->user_runtime)
- {
- if (!valid_runtime(paths->user_runtime))
- {
- free(paths->user_runtime);
- paths->user_runtime = NULL;
- }
- }
- paths->search_data = get_searchdirs("XDG_DATA_DIRS",
- "/usr/local/share:/usr/share");
- if (!paths->search_data)
- {
- return false;
- }
- paths->search_config = get_searchdirs("XDG_CONFIG_DIRS", "/etc/xdg");
- if (!paths->search_config)
- {
- return false;
- }
- return true;
-}
-
-static void cleanup_dir(char* path, size_t len)
-{
- while (len > 1 && path[len - 1] == '/')
- {
- path[--len] = '\0';
- }
-}
-
-static char* get_homedir(void)
-{
- const char* data = getenv("HOME");
- size_t len;
- char* ret;
- if (data && *data)
- {
- len = strlen(data);
- ret = malloc(len + 1);
- if (ret)
- {
- memcpy(ret, data, len + 1);
- cleanup_dir(ret, len);
- }
- return ret;
- }
- for (;;)
- {
- long size = sysconf(_SC_GETPW_R_SIZE_MAX);
- char* buf;
- struct passwd entry, *result = NULL;
- assert(size != -1);
- if (size == -1)
- {
- break;
- }
- buf = malloc(size);
- if (!buf)
- {
- break;
- }
- if (getpwuid_r(getuid(), &entry, buf, size, &result) != 0)
- {
- /* Error getting entry */
- free(buf);
- break;
- }
- if (!result)
- {
- /* No entry found */
- free(buf);
- break;
- }
- len = strlen(entry.pw_dir);
- if (len == 0)
- {
- free(buf);
- break;
- }
- ret = malloc(len + 1);
- if (!ret)
- {
- free(buf);
- return NULL;
- }
- memcpy(ret, entry.pw_dir, len + 1);
- free(buf);
- cleanup_dir(ret, len);
- return ret;
- }
- /* When all else fails */
- return strdup("/");
-}
-
-char* get_userdir(const char* envvar, homedir_t* homedir, const char* addon)
-{
- const char* data = getenv(envvar);
- size_t len;
- char* ret;
- assert(!addon || addon[0] == '/');
- if (data && *data)
- {
- len = strlen(data);
- ret = malloc(len + 1);
- if (ret)
- {
- memcpy(ret, data, len + 1);
- cleanup_dir(ret, len);
- }
- return ret;
- }
- if (!homedir && !addon)
- {
- return NULL;
- }
- assert(homedir && addon);
- if (!homedir->path)
- {
- homedir->path = get_homedir();
- if (!homedir->path)
- {
- return NULL;
- }
- homedir->len = strlen(homedir->path);
- }
- len = homedir->len + strlen(addon);
- ret = malloc(len + 1);
- if (ret)
- {
- memcpy(ret, homedir->path, homedir->len);
- if (homedir->len > 1)
- {
- assert(homedir->path[homedir->len - 1] != '/');
- memcpy(ret + homedir->len, addon, (len - homedir->len) + 1);
- }
- else
- {
- assert(homedir->path[0] == '/');
- len--;
- memcpy(ret + homedir->len, addon + 1, len - homedir->len);
- ret[len] = '\0';
- }
- }
- return ret;
-}
-
-static void cleanup_dirs(char** dirs)
-{
- char** d;
- for (d = dirs; *d; d++)
- {
- cleanup_dir(*d, strlen(*d));
- }
-}
-
-static char** splitlist(const char* str)
-{
- size_t count = 0, size = 2;
- char** ret = calloc(size + 1, sizeof(char*));
- const char* last = str, * pos;
- if (!ret)
- {
- return NULL;
- }
- for (;;)
- {
- size_t len;
- pos = strchr(last, ':');
- if (!pos)
- {
- pos = last + strlen(last);
- }
-
- if (count == size)
- {
- size_t ns = size * 2;
- char** tmp = realloc(ret, (ns + 1) * sizeof(char*));
- if (!tmp)
- {
- free_list(ret);
- return NULL;
- }
- memset(tmp + size, 0, (ns + 1 - size) * sizeof(char*));
- ret = tmp;
- size = ns;
- }
- len = pos - last;
- ret[count] = strdup_len(last, len);
- if (!ret[count])
- {
- free_list(ret);
- return NULL;
- }
- count++;
-
- if (*pos)
- {
- last = pos + 1;
- }
- else
- {
- break;
- }
- }
- assert(!ret[count]);
- return ret;
-}
-
-char** get_searchdirs(const char* envvar, const char* default_list)
-{
- const char* data = getenv(envvar);
- char** ret;
- bool cleanup;
- if (data && *data)
- {
- ret = splitlist(data);
- cleanup = true;
- }
- else
- {
- ret = splitlist(default_list);
- cleanup = false;
- }
- if (!ret)
- {
- return NULL;
- }
- if (cleanup)
- {
- cleanup_dirs(ret);
- }
- return ret;
-}
-
-bool valid_runtime(const char* path)
-{
- struct stat buf;
- if (stat(path, &buf) != 0)
- {
- return false;
- }
- if (buf.st_uid != getuid())
- {
- return false;
- }
- if ((buf.st_mode & 0777) != 0700)
- {
- return false;
- }
- return true;
-}
-
-char* build_path(paths_t* paths, paths_source_t source, ...)
-{
- va_list args;
- bool first = true;
- dynstr_t* ret;
- const char* base = paths_user_dir(paths, source), * part;
- assert(base);
- ret = dynstr_new_str(base);
- if (!ret)
- {
- return NULL;
- }
- va_start(args, source);
- while ((part = va_arg(args, const char*)))
- {
- if (first)
- {
- dynstr_appendc(ret, '/');
- first = false;
- }
- dynstr_append(ret, part);
- }
- return dynstr_done(ret);
-}
-
-static bool need_glob(char* match)
-{
- const char* magic_chars = "*?[]{}";
- char* s;
- bool need_unescape = false;
- for (s = match; *s; s++)
- {
- if (*s == '\\')
- {
- s++;
- need_unescape = true;
- }
- else if (strchr(magic_chars, *s))
- {
- return true;
- }
- }
- if (need_unescape)
- {
- unescape(match);
- }
- return false;
-}
-
-void paths_find(paths_t* paths, paths_source_t source,
- const char* match, paths_find_callback_t find_callback,
- void* userdata)
-{
- search_t* search;
- search = get_search(paths->search);
- search->dir = paths_user_dir(paths, source);
- search->dirs = paths_search_dirs(paths, source);
- search->match = strdup(match);
- search->find_callback = find_callback;
- search->userdata = userdata;
- if (!search->match)
- {
- find_callback(userdata, NULL);
- put_search(paths->search, search);
- return;
- }
- if (*search->match == '/')
- {
- size_t len = strlen(search->match);
- memmove(search->match, search->match + 1, len);
- }
- if (!*search->match)
- {
- find_callback(userdata, NULL);
- put_search(paths->search, search);
- return;
- }
- search->plain = !need_glob(search->match);
- if (search->plain)
- {
- search->match_name = strrchr(search->match, '/');
- if (search->match_name)
- {
- search->match_name++;
- }
- else
- {
- search->match_name = search->match;
- }
- }
- safe_fifo_push(paths->search->queue, search);
-}
-
-search_t* get_search(searchargs_t* args)
-{
- size_t i;
- thread_lock_lock(args->lock);
- for (i = 0; i < SEARCH_CACHE_SIZE; i++)
- {
- if (args->cache[i])
- {
- search_t* ret = args->cache[i];
- args->cache[i] = NULL;
- thread_lock_unlock(args->lock);
- return ret;
- }
- }
- thread_lock_unlock(args->lock);
- return malloc(sizeof(search_t));
-}
-
-void put_search(searchargs_t* args, search_t* search)
-{
- size_t i;
- thread_lock_lock(args->lock);
- for (i = 0; i < SEARCH_CACHE_SIZE; i++)
- {
- if (!args->cache[i])
- {
- args->cache[i] = search;
- free_search_data(search);
- thread_lock_unlock(args->lock);
- return;
- }
- }
- thread_lock_unlock(args->lock);
- free_search_data(search);
- free(search);
-}
-
-void free_search_data(search_t* search)
-{
- free(search->match);
-}
-
-static bool search_dir(search_t* search, const char* dir)
-{
- bool canceled = false;
- if (search->plain)
- {
- DIR* dh;
- struct dirent* d;
- char* path = NULL;
- size_t dirlen = strlen(dir);
- size_t matchlen = strlen(search->match);
- if (search->match == search->match_name)
- {
- path = malloc(dirlen + 1 + matchlen + 1);
- if (!path)
- {
- return false;
- }
- memcpy(path, dir, dirlen + 1);
- }
- else
- {
- size_t subdirlen = search->match_name - 1 - search->match;
- matchlen = strlen(search->match_name);
- path = malloc(dirlen + 1 + subdirlen + 1 + matchlen + 1);
- assert(search->match_name > search->match);
- assert(search->match_name[-1] == '/');
- if (!path)
- {
- return false;
- }
- memcpy(path, dir, dirlen);
- path[dirlen++] = '/';
- memcpy(path + dirlen, search->match, subdirlen);
- dirlen += subdirlen;
- path[dirlen] = '\0';
- }
- dh = opendir(path);
- if (!dh)
- {
- free(path);
- return false;
- }
- path[dirlen] = '/';
- while ((d = readdir(dh)))
- {
- if (strcmp(d->d_name, search->match_name) == 0)
- {
- memcpy(path + dirlen + 1, search->match_name, matchlen + 1);
- if (!search->find_callback(search->userdata, path))
- {
- canceled = true;
- break;
- }
- }
- }
- closedir(dh);
- free(path);
- }
- else
- {
- glob_t data;
- dynstr_t* str;
- char* pattern;
- str = dynstr_new_str(dir);
- if (!str)
- {
- return false;
- }
- if (!dynstr_escape(str, "*?[]{}") || !dynstr_appendc(str, '/') ||
- !dynstr_append(str, search->match))
- {
- dynstr_free(str);
- return false;
- }
- pattern = dynstr_done(str);
- memset(&data, 0, sizeof(glob_t));
- if (glob(pattern, GLOB_NOSORT, NULL, &data) == 0)
- {
- size_t i;
- for (i = 0; i < data.gl_pathc; i++)
- {
- if (!search->find_callback(search->userdata, data.gl_pathv[i]))
- {
- canceled = true;
- break;
- }
- }
- globfree(&data);
- }
- free(pattern);
- }
- return canceled;
-}
-
-void* searcher(void* _args)
-{
- searchargs_t* args = _args;
- size_t i;
-
- while (!args->quit)
- {
- search_t* search = safe_fifo_pop(args->queue);
- bool canceled = false;
- if (!search->match)
- {
- free(search);
- assert(args->quit);
- break;
- }
- do
- {
- if (search->dir)
- {
- if (args->quit ||
- (canceled = search_dir(search, search->dir)))
- {
- break;
- }
- }
- if (search->dirs)
- {
- const char** dir;
- for (dir = search->dirs; *dir; dir++)
- {
- if (args->quit ||
- (canceled = search_dir(search, *dir)))
- {
- break;
- }
- }
- }
- } while (false);
- if (!canceled)
- {
- search->find_callback(search->userdata, NULL);
- }
- put_search(args, search);
- }
-
- /* Free all items in the queue */
- for (;;)
- {
- search_t* search = safe_fifo_trypop(args->queue);
- if (!search)
- {
- break;
- }
- put_search(args, search);
- }
-
- safe_fifo_unref(args->queue);
- thread_lock_free(args->lock);
- for (i = 0; i < SEARCH_CACHE_SIZE; i++)
- {
- if (args->cache[i])
- {
- free_search_data(args->cache[i]);
- free(args->cache[i]);
- }
- }
- free(args);
- return NULL;
-}
diff --git a/src/paths.h b/src/paths.h
deleted file mode 100644
index 8be56f8..0000000
--- a/src/paths.h
+++ /dev/null
@@ -1,261 +0,0 @@
-/**
- * \file paths.h
- * XDG Directory Specification implementation
- */
-
-#ifndef PATHS_H
-#define PATHS_H
-
-/**
- * Opaque type for paths
- */
-typedef struct paths_t paths_t;
-
-/**
- * Opaque type for a currently open file
- */
-typedef struct paths_file_t paths_file_t;
-
-/**
- * The different sources or directories if you will that are in the
- * specification.
- */
-typedef enum paths_source_t
-{
- /** directory for data files */
- PATHS_DATA,
- /** directory for config files */
- PATHS_CONFIG,
- /** directory for cache files */
- PATHS_CACHE,
- /** directory for runtime files */
- PATHS_RUNTIME,
-} paths_source_t;
-
-/** Do not block write calls */
-#define PATHS_NONBLOCK (0x01)
-/** Append on each write */
-#define PATHS_APPEND (0x02)
-/** Create file if needed */
-#define PATHS_CREATE (0x04)
-/** Truncate file when opening */
-#define PATHS_TRUNCATE (0x08)
-/** If PATHS_CREATE is set, give error if file already exists */
-#define PATHS_EXCLUSIVE (0x10)
-
-/**
- * Create a new paths.
- * Initializes the list of directories from enviroment variables.
- * Returned object has a reference count of one.
- * Threadsafe.
- * Returns NULL in case of allocation errors.
- * @return a new paths
- */
-MALLOC paths_t* paths_new(void);
-
-/**
- * Increase the reference count for paths.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- */
-NONULL void paths_ref(paths_t* paths);
-
-/**
- * Decrease the reference count for paths and free if it reached zero.
- * Threadsafe.
- * @param paths paths object, may be NULL
- */
-void paths_unref(paths_t* paths);
-
-/**
- * Get single directory where user-specific files should be written.
- * May only return NULL for PATHS_RUNTIME all other sources are guaranteed to
- * return a non-NULL string. They may however return non-existant paths.
- * PATHS_RUNTIME will return NULL if no such path was found as it has no
- * default.
- * If you wish paths to be created if missing, please use paths_write().
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @param source requested directory
- * @return absolute path to directory or NULL
- */
-NONULL const char* paths_user_dir(paths_t* paths, paths_source_t source);
-
-/**
- * Get preference ordered list of directories to search for files.
- * All paths are absolute and NULL terminated. The list itself is NULL
- * terminated and may never be empty. Path returned by paths_user_dir() for
- * the same source is not included.
- * Will return NULL for PATHS_CACHE and PATHS_RUNTIME as they are only
- * user-specific writable directories.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @param source requested directory
- * @return NULL terminated list of absolute paths to search or NULL
- */
-NONULL const char** paths_search_dirs(paths_t* paths, paths_source_t source);
-
-/**
- * Callback called with the result(s) from paths_find(). May be called from
- * any thread. Return false to cancel the search, true to continue until all
- * has been found. filename will be NULL when no more matches was found.
- * The filename pointer is only valid until the callback has returned.
- * @param userdata userdata given to paths_find()
- * @param filenamme found file or NULL if no more results
- * @return true to continue the search, false to not */
-typedef bool (* paths_find_callback_t)(void* userdata, const char* filename);
-
-/**
- * Asynchronous search for matching files in directories for source.
- * Returned in preference order, first paths_user_dir() and then
- * paths_search_dirs().
- * The callback is always called at least once.
- * Match may use *, ?, [] and {} glob matches. It may also contain / characters
- * to search in subdirectories
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @param source requested directory
- * @param match glob pattern to search with. may not be NULL
- * @param find_callback callback to call when finding results, may not be NULL
- * @param userdata argument to callback, may be NULL
- */
-NONULL_ARGS(1, 3, 4)
-void paths_find(paths_t* paths, paths_source_t source, const char* match,
- paths_find_callback_t find_callback, void* userdata);
-
-/**
- * Synchronous setup to create file in directory.
- * Returns NULL in case of error, see errno for details.
- * Depending on source and convention, the actual file may not be created with
- * the given name until paths_file_close().
- * The file will eventually always end up in paths_user_dir() for directory.
- * If any part of the target path is missing it will be created if possible.
- * If flags contains PATHS_CREATE then the 5th argument mode_t mode must be
- * given, just as for open().
- * Threadsafe but what happens if two threads at the same time opens the same
- * target file is up to the platforms locking mechanism.
- * The returned paths_file_t is not threadsafe.
- * @param paths paths object, may not be NULL
- * @param source target directoru
- * @param filename relative path of target file, may not be NULL
- * @param flags flags
- * @param ... mode if PATHS_CREATE is included in flags
- * @return a file reference or NULL
- */
-NONULL paths_file_t* paths_write(paths_t* paths, paths_source_t source,
- const char* filename, unsigned int flags,
- ...);
-
-/**
- * Write data to file.
- * Works just as libc write().
- * Not threadsafe,
- * the paths_file_t must only be accessed by one thread at a time.
- * @param file target file, may not be NULL
- * @param data data to write, may be NULL if size is zero
- * @param size the maximum number of bytes to read from data
- * @return number of bytes written to target
- */
-NONULL_ARGS(1)
-ssize_t paths_file_write(paths_file_t* file, const void* data, size_t size);
-
-/**
- * Close and finish saving file.
- * If the file was saved successfully, the paths_file_t object is freed.
- * If the save failed in any way, the object is not freed so you must call
- * paths_file_abort() to finish up (or call paths_file_close() again and
- * hope for better luck this time).
- * Not threadsafe,
- * the paths_file_t must only be accessed by one thread at a time.
- * @param file target file, may not be NULL
- * @return zero on success, non-zero in case of error
- */
-NONULL int paths_file_close(paths_file_t* file);
-
-/**
- * Abort saving the file. The target file is removed unless the original version
- * before paths_write() could be restored.
- * Not threadsafe,
- * the paths_file_t must only be accessed by one thread at a time.
- * @param file target file, may be NULL
- */
-void paths_file_abort(paths_file_t* file);
-
-/**
- * Get absolute path to directory where user-specific data files should be
- * written. Returned directory may not exist.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @return absolute path to data directory
- */
-static inline NONULL const char* paths_user_data(paths_t* paths)
-{
- return paths_user_dir(paths, PATHS_DATA);
-}
-
-/**
- * Get absolute path to directory where user-specific config files should be
- * written. Returned directory may not exist.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @return absolute path to config directory
- */
-static inline NONULL const char* paths_user_config(paths_t* paths)
-{
- return paths_user_dir(paths, PATHS_CONFIG);
-}
-
-/**
- * Get absolute path to directory where user-specific cache files should be
- * written. Returned directory may not exist.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @return absolute path to cache directory
- */
-static inline NONULL const char* paths_user_cache(paths_t* paths)
-{
- return paths_user_dir(paths, PATHS_CACHE);
-}
-
-/**
- * Get absolute path to directory where user-specific runtime files should be
- * written.
- * If not defined in enviroment or not valid NULL is returned.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @return absolute path to runtime directory or NULL
- */
-static inline NONULL const char* paths_user_runtime(paths_t* paths)
-{
- return paths_user_dir(paths, PATHS_RUNTIME);
-}
-
-/**
- * Get preference ordered list of directories to search for data files.
- * All paths are absolute and NULL terminated. The list itself is NULL
- * terminated and may never be empty. Path returned by paths_user_data()
- * is not included.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @return NULL terminated list of absolute paths to search
- */
-static inline NONULL const char** paths_data(paths_t* paths)
-{
- return paths_search_dirs(paths, PATHS_DATA);
-}
-
-/**
- * Get preference ordered list of directories to search for config files.
- * All paths are absolute and NULL terminated. The list itself is NULL
- * terminated and may never be empty. Path returned by paths_user_config()
- * is not included.
- * Threadsafe.
- * @param paths paths object, may not be NULL
- * @return NULL terminated list of absolute paths to search
- */
-static inline NONULL const char** paths_config(paths_t* paths)
-{
- return paths_search_dirs(paths, PATHS_CONFIG);
-}
-
-#endif /* PATHS_H */
diff --git a/src/ref.h b/src/ref.h
deleted file mode 100644
index ea36ec7..0000000
--- a/src/ref.h
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * \file ref.h
- * Thread-safe reference counter.
- */
-
-#ifndef REF_H
-#define REF_H
-
-#if __GNUC__ >= 4
-# define REF_DECLARE() volatile int _ref
-# define REF_INIT(ptr) \
- ((ptr)->_ref = 1)
-# define REF_FREE(ptr)
-# define REF_INC(ptr) \
- __sync_add_and_fetch(&((ptr)->_ref), 1)
-# define REF_DEC(ptr) \
- (__sync_sub_and_fetch(&((ptr)->_ref), 1) <= 0)
-
-#else
-# warning Unsupported compiler, horrible fallback threadsafe reference counters
-
-# include "thread.h"
-
-/**
- * Declare reference counter. Use in struct declaration.
- */
-# define REF_DECLARE() int _ref; thread_lock_t* _reflock
-/**
- * Initialize reference counter. Must be first MACRO used.
- * @param ptr pointer to the struct, may not be NULL
- * @return false in case of allocation errors
- */
-# define REF_INIT(ptr) \
- _ref_init(&((ptr)->_ref), &((ptr)->_reflock))
-/**
- * Free reference counter.
- * @param ptr pointer to the struct, may not be NULL
- */
-# define REF_FREE(ptr) thread_lock_free((ptr)->_reflock)
-/**
- * Increase reference counter.
- * @param ptr pointer to the struct, may not be NULL
- */
-# define REF_INC(ptr) _ref_inc(&((ptr)->_ref), (ptr)->_reflock)
-/**
- * Decrease reference counter.
- * @param ptr pointer to the struct, may not be NULL
- * @return true if reference counter now is zero
- */
-# define REF_DEC(ptr) _ref_dec(&((ptr)->_ref), (ptr)->_reflock)
-
-/**
- * Fallback implementation of REF_INIT.
- * @param value pointer to reference counter, may not be NULL
- * @param lock pointer to reference counter lock, may not be NULL
- * @return true if both pointers was initialized correctly
- */
-static inline NONULL bool _ref_init(int* value, thread_lock_t** lock)
-{
- assert(value);
- assert(lock);
- *value = 1;
- *lock = thread_lock_new();
- return *lock;
-}
-
-/**
- * Fallback implementation of REF_INC.
- * @param value pointer to reference counter, may not be NULL
- * @param lock reference counter lock, may not be NULL
- */
-static inline NONULL void _ref_inc(int* value, thread_lock_t* lock)
-{
- thread_lock_lock(lock);
- (*value)++;
- thread_lock_unlock(lock);
-}
-
-/**
- * Fallback implementation of REF_DEC.
- * @param value pointer to reference counter, may not be NULL
- * @param lock reference counter lock, may not be NULL
- * @return true if reference counter was decreased to 0
- */
-static inline NONULL bool _ref_dec(int* value, thread_lock_t* lock)
-{
- bool ret;
- thread_lock_lock(lock);
- ret = --(*value) == 0;
- thread_lock_unlock(lock);
- return ret;
-}
-#endif
-
-#endif /* REF_H */
diff --git a/src/safe_fifo.c b/src/safe_fifo.c
deleted file mode 100644
index a294d1c..0000000
--- a/src/safe_fifo.c
+++ /dev/null
@@ -1,151 +0,0 @@
-#include "common.h"
-
-#include "safe_fifo.h"
-#include "thread.h"
-#include "ref.h"
-
-#include <string.h>
-
-struct safe_fifo_t
-{
- REF_DECLARE();
- thread_lock_t* lock;
- thread_cond_t* empty;
- size_t pos, fill, size;
- void** data;
-};
-
-safe_fifo_t* safe_fifo_new(void)
-{
- safe_fifo_t* fifo = calloc(1, sizeof(safe_fifo_t));
- if (fifo)
- {
- if (!REF_INIT(fifo))
- {
- free(fifo);
- return NULL;
- }
- fifo->lock = thread_lock_new();
- fifo->empty = thread_cond_new();
- if (!fifo->lock || !fifo->empty)
- {
- thread_lock_free(fifo->lock);
- thread_cond_free(fifo->empty);
- free(fifo);
- return NULL;
- }
- }
- return fifo;
-}
-
-void safe_fifo_ref(safe_fifo_t* fifo)
-{
- assert(fifo);
- REF_INC(fifo);
-}
-
-void safe_fifo_unref(safe_fifo_t* fifo)
-{
- if (fifo && REF_DEC(fifo))
- {
- thread_cond_free(fifo->empty);
- thread_lock_free(fifo->lock);
- free(fifo->data);
- REF_FREE(fifo);
- free(fifo);
- }
-}
-
-void safe_fifo_push(safe_fifo_t* fifo, void* item)
-{
- thread_lock_lock(fifo->lock);
- do
- {
- if (fifo->pos > 0)
- {
- fifo->fill -= fifo->pos;
- memmove(fifo->data, fifo->data + fifo->pos,
- fifo->fill * sizeof(void*));
- fifo->pos = 0;
- }
- if (fifo->fill == fifo->size)
- {
- size_t ns = MAX(4, fifo->size * 2);
- void** tmp = realloc(fifo->data, ns * sizeof(void*));
- if (!tmp)
- {
- break;
- }
- fifo->size = ns;
- fifo->data = tmp;
- }
- fifo->data[fifo->fill++] = item;
- } while (false);
- thread_cond_signal(fifo->empty);
- thread_lock_unlock(fifo->lock);
-}
-
-void* safe_fifo_pop(safe_fifo_t* fifo)
-{
- void* ret;
- assert(fifo);
- thread_lock_lock(fifo->lock);
- for (;;)
- {
- if (fifo->fill > 0)
- {
- ret = fifo->data[fifo->pos++];
- if (fifo->pos == fifo->fill)
- {
- fifo->pos = fifo->fill = 0;
- }
- break;
- }
- thread_cond_wait(fifo->empty, fifo->lock);
- }
- thread_lock_unlock(fifo->lock);
- return ret;
-}
-
-void* safe_fifo_trypop(safe_fifo_t* fifo)
-{
- void* ret = NULL;
- assert(fifo);
- thread_lock_lock(fifo->lock);
- if (fifo->fill > 0)
- {
- ret = fifo->data[fifo->pos++];
- if (fifo->pos == fifo->fill)
- {
- fifo->pos = fifo->fill = 0;
- }
- }
- thread_lock_unlock(fifo->lock);
- return ret;
-}
-
-void* safe_fifo_timedpop(safe_fifo_t* fifo, const struct timespec* abstime)
-{
- void* ret;
- assert(fifo);
- thread_lock_lock(fifo->lock);
- for (;;)
- {
- if (fifo->fill > 0)
- {
- ret = fifo->data[fifo->pos++];
- if (fifo->pos == fifo->fill)
- {
- fifo->pos = fifo->fill = 0;
- }
- break;
- }
- if (!thread_cond_timedwait(fifo->empty, fifo->lock, abstime))
- {
- ret = NULL;
- break;
- }
- }
- thread_lock_unlock(fifo->lock);
- return ret;
-}
diff --git a/src/safe_fifo.h b/src/safe_fifo.h
deleted file mode 100644
index 4644c33..0000000
--- a/src/safe_fifo.h
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * \file safe_fifo.h
- * Thread-safe FIFO queue.
- */
-
-#ifndef SAFE_FIFO_H
-#define SAFE_FIFO_H
-
-#include "timespec.h"
-
-/**
- * Opaque type describing a thread-safe FIFO queue
- */
-typedef struct safe_fifo_t safe_fifo_t;
-
-/**
- * Create a new queue.
- * The new queue will have a reference count of one.
- * Returns NULL in case of allocation errors.
- * @return a new queue
- */
-MALLOC safe_fifo_t* safe_fifo_new(void);
-
-/**
- * Increase the reference count on the queue.
- * @param fifo queue, may not be NULL
- */
-NONULL void safe_fifo_ref(safe_fifo_t* fifo);
-
-/**
- * Decrease the reference count on the queue and free if it goes down to zero.
- * @param fifo queue, may be NULL
- */
-void safe_fifo_unref(safe_fifo_t* fifo);
-
-/**
- * Push an item on the queue.
- * Will never block. In case of allocation errors, new items are ignored.
- * @param fifo queue, may not be NULL
- * @param item pointer to add to queue, may not be NULL
- */
-NONULL void safe_fifo_push(safe_fifo_t* fifo, void* item);
-
-/**
- * Pop an item from the queue.
- * Will block until there is an item available.
- * @param fifo queue, may not be NULL
- * @return oldest item pushed onto the queue
- */
-NONULL void* safe_fifo_pop(safe_fifo_t* fifo);
-
-/**
- * Try to pop an item from the queue.
- * Will not wait until there is an item available.
- * @param fifo queue, may not be NULL
- * @return oldest item in queue if any otherwise NULL
- */
-NONULL void* safe_fifo_trypop(safe_fifo_t* fifo);
-
-/**
- * Try to pop an item from the queue.
- * Waits until abstime has occurred before giving up and returning NULL.
- * Use thread_abstime() to fill out the abstime.
- * @param fifo queue, may not be NULL
- * @param abstime absolute time when to give up
- * @return oldest item in queue if any otherwise NULL
- */
-NONULL void* safe_fifo_timedpop(safe_fifo_t* fifo,
- const struct timespec *abstime);
-
-#endif /* SAFE_FIFO_H */
diff --git a/src/strutil.c b/src/strutil.c
deleted file mode 100644
index 4502b83..0000000
--- a/src/strutil.c
+++ /dev/null
@@ -1,123 +0,0 @@
-#include "common.h"
-
-#include "strutil.h"
-
-#include <string.h>
-
-char* strdup_len(const char* src, size_t len)
-{
- char* ret = malloc(len + 1);
- if (ret)
- {
- memcpy(ret, src, len);
- ret[len] = '\0';
- }
- return ret;
-}
-
-static inline bool escaped(char* start, char* cur)
-{
- return cur > start && cur[-1] == '\\' && !escaped(start, cur - 1);
-}
-
-void unescape(char* str)
-{
- char* end = str + strlen(str);
- char* s;
- for (s = end - 1; s >= str; s--)
- {
- if (*s == '\\' && s < end - 1 && !escaped(str, s))
- {
- end--;
- memmove(s, s + 1, end - s);
- }
- }
- *end = '\0';
-}
-
-char* escape(const char* str, const char* chars)
-{
- dynstr_t* d = dynstr_new_str(str);
- if (d)
- {
- if (dynstr_escape(d, chars))
- {
- return dynstr_done(d);
- }
- dynstr_free(d);
- }
- return NULL;
-}
-
-bool dynstr_escape(dynstr_t* str, const char* chars)
-{
- char* s;
- for (s = str->data + str->len - 1; s >= str->data; s--)
- {
- if (*s == '\\' || strchr(chars, *s))
- {
- size_t pos = s - str->data;
- if (!dynstr_appendc(str, '/'))
- {
- return false;
- }
- s = str->data + pos;
- memmove(s + 1, s, str->len - (pos + 1));
- *s = '\\';
- }
- }
- return true;
-}
-
-static void free_list(char** list, size_t count)
-{
- char** l;
- for (l = list; count-- > 0; l++)
- {
- free(*l);
- }
- free(list);
-}
-
-char** split_len(const char* start, const char* end, char delim)
-{
- char** ret;
- size_t count = 0, size = 2;
- assert(start <= end);
- ret = malloc((size + 1) * sizeof(char*));
- if (ret)
- {
- const char* last = start;
- for (;;)
- {
- const char* p;
- for (p = last; p < end && *p != delim; p++);
- if (count == size)
- {
- size_t ns = size * 2;
- char** tmp = realloc(ret, (ns + 1) * sizeof(char*));
- if (!tmp)
- {
- free_list(ret, count);
- return NULL;
- }
- size = ns;
- ret = tmp;
- }
- ret[count] = strdup_len(last, p - last);
- if (!ret[count])
- {
- free_list(ret, count);
- return NULL;
- }
- count++;
- if (p == end)
- {
- break;
- }
- last = p + 1;
- }
- ret[count] = NULL;
- }
- return ret;
-}
diff --git a/src/strutil.h b/src/strutil.h
deleted file mode 100644
index f4dea54..0000000
--- a/src/strutil.h
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * \file strutil.h
- * A collection of string utilities.
- */
-
-#ifndef STRUTIL_H
-#define STRUTIL_H
-
-#include "dynstr.h"
-
-#include <string.h>
-
-/**
- * Allocate a string initialized with the len first bytes of src.
- * Similar to strndup but it doesn't check for '\0' in src.
- * Returns NULL in case of allocation errors.
- * @param src string to copy len bytes from, may not be NULL
- * @param len bytes to copy from src
- * @return newly allocate null-terminated string
- */
-MALLOC NONULL char* strdup_len(const char* src, size_t len);
-
-/**
- * Unescape (remove backslashes from) a null-terminated string inline.
- * Does not have any special handling for sequences like \\n, \\t or \\0.
- * @param str null-terminated string to remove backslashes from, may not be NULL
- */
-NONULL void unescape(char* str);
-
-/**
- * Escape all listed characters if they appear in string with backslash.
- * Also escapes backslash.
- * Returns NULL in case of allocation errors.
- * @param str null-terminated string to escape characters in if needed,
- * may not be NULL
- * @param chars null-terminated string containing all characters that need to
- * be escaped, may not be NULL
- * @return newly allocated null-terminated copy of string with
- * characters escaped
- */
-MALLOC NONULL char* escape(const char* str, const char* chars);
-
-/**
- * Escape all listed characters if they appear in the dynamic string with
- * backslash inline. Also escapes backslash.
- * @param str dynamic string to escape characters in if needed,
- * may not be NULL
- * @param chars null-terminated string containing all characters that need to
- * be escaped, may not be NULL
- * @return false in case of allocation errors
- */
-NONULL bool dynstr_escape(dynstr_t* str, const char* chars);
-
-/**
- * Split string by delimiter.
- * Returned list will always contain at least one item and will be
- * null-terminated.
- * Returns NULL in case of allocation errors.
- * @param start start of string to split by delimiter
- * @param end end of string to split by delimiter
- * @param delim delimiter to use
- * @return null-terminated list of strings or NULL
- */
-NONULL char** split_len(const char* start, const char* end, char delim);
-
-/**
- * Split string by delimiter.
- * Returned list will always contain at least one item and will be
- * null-terminated.
- * Returns NULL in case of allocation errors.
- * @param str string to split by delimiter
- * @param delim delimiter to use
- * @return null-terminated list of strings or NULL
- */
-NONULL static inline char** split(const char* str, char delim)
-{
- assert(str);
- return split_len(str, str + strlen(str), delim);
-}
-
-#endif /* STRUTIL_H */
diff --git a/src/thread-pthread.c b/src/thread-pthread.c
deleted file mode 100644
index 6e31cf9..0000000
--- a/src/thread-pthread.c
+++ /dev/null
@@ -1,343 +0,0 @@
-#include "common.h"
-
-#include "thread.h"
-
-#include <errno.h>
-#include <pthread.h>
-
-#if !HAVE_PTHREAD_YIELD
-#include <sched.h>
-#endif
-
-#if !HAVE_CLOCK_GETTIME
-# include <sys/time.h>
-#endif
-
-#ifdef DEBUG
-#define ABORTFAIL(x) \
- do { \
- if ((x)) abort(); \
- } while (false)
-#else
-#define ABORTFAIL(x) (x)
-#endif
-
-bool thread_new(thread_run_t run, void* userdata)
-{
- pthread_t thread;
- pthread_attr_t attr;
- assert(run);
- if (pthread_attr_init(&attr))
- {
- return NULL;
- }
- if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED))
- {
- pthread_attr_destroy(&attr);
- return NULL;
- }
- if (pthread_create(&thread, &attr, run, userdata))
- {
- pthread_attr_destroy(&attr);
- return false;
- }
- pthread_attr_destroy(&attr);
- return true;
-}
-
-struct thread_t
-{
- pthread_t thread;
-};
-
-thread_t* thread_joinable_new(thread_run_t run, void* userdata)
-{
- thread_t* ret = malloc(sizeof(thread_t));
- pthread_attr_t attr;
- assert(run);
- if (!ret)
- {
- return NULL;
- }
- if (pthread_attr_init(&attr))
- {
- free(ret);
- return NULL;
- }
- if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE))
- {
- pthread_attr_destroy(&attr);
- free(ret);
- return NULL;
- }
- if (pthread_create(&ret->thread, &attr, run, userdata))
- {
- pthread_attr_destroy(&attr);
- free(ret);
- return NULL;
- }
- pthread_attr_destroy(&attr);
- return ret;
-}
-
-void* thread_join(thread_t* thread)
-{
- void* ret;
- assert(thread);
- ABORTFAIL(pthread_join(thread->thread, &ret));
- free(thread);
- return ret;
-}
-
-struct thread_id_t
-{
- pthread_t thread;
-};
-
-thread_id_t* thread_id_new(thread_t* thread)
-{
- thread_id_t* id = malloc(sizeof(thread_id_t));
- if (id)
- {
- id->thread = thread ? thread->thread : pthread_self();
- }
- return id;
-}
-
-void thread_id_free(thread_id_t* id)
-{
- free(id);
-}
-
-bool thread_id_is_current(thread_id_t* id)
-{
- assert(id);
- return id->thread == pthread_self();
-}
-
-void thread_yield(void)
-{
-#if HAVE_PTHREAD_YIELD
-# if PTHREAD_YIELD_VOID
- pthread_yield();
-# else
- ABORTFAIL(pthread_yield());
-# endif
-#else
- ABORTFAIL(sched_yield());
-#endif
-}
-
-struct thread_lock_t
-{
- pthread_mutex_t mutex;
-};
-
-thread_lock_t* thread_lock_new(void)
-{
- thread_lock_t* lock = malloc(sizeof(thread_lock_t));
- if (!lock)
- {
- return lock;
- }
- for (;;)
- {
-#ifdef DEBUG
- pthread_mutexattr_t attr;
- if (pthread_mutexattr_init(&attr))
- {
- break;
- }
- pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
- if (pthread_mutex_init(&lock->mutex, &attr))
- {
- pthread_mutexattr_destroy(&attr);
- break;
- }
- pthread_mutexattr_destroy(&attr);
-#else
- if (pthread_mutex_init(&lock->mutex, NULL))
- {
- break;
- }
-#endif
- return lock;
- }
- free(lock);
- return NULL;
-}
-
-thread_lock_t* thread_lock_recursive_new(void)
-{
- thread_lock_t* lock = malloc(sizeof(thread_lock_t));
- for (;;)
- {
- if (!lock)
- {
- break;
- }
- pthread_mutexattr_t attr;
- if (pthread_mutexattr_init(&attr))
- {
- break;
- }
- pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
- if (pthread_mutex_init(&lock->mutex, &attr))
- {
- pthread_mutexattr_destroy(&attr);
- break;
- }
- pthread_mutexattr_destroy(&attr);
- return lock;
- }
- free(lock);
- return NULL;
-}
-
-void thread_lock_free(thread_lock_t* lock)
-{
- if (lock)
- {
- pthread_mutex_destroy(&lock->mutex);
- free(lock);
- }
-}
-
-void thread_lock_lock(thread_lock_t* lock)
-{
- assert(lock);
- ABORTFAIL(pthread_mutex_lock(&lock->mutex));
-}
-
-void thread_lock_unlock(thread_lock_t* lock)
-{
- assert(lock);
- ABORTFAIL(pthread_mutex_unlock(&lock->mutex));
-}
-
-struct thread_cond_t
-{
- pthread_cond_t cond;
-};
-
-thread_cond_t* thread_cond_new(void)
-{
- thread_cond_t* cond = malloc(sizeof(thread_cond_t));
- if (cond)
- {
- if (pthread_cond_init(&cond->cond, NULL))
- {
- free(cond);
- return NULL;
- }
- }
- return cond;
-}
-
-void thread_cond_free(thread_cond_t* cond)
-{
- if (cond)
- {
- pthread_cond_destroy(&cond->cond);
- free(cond);
- }
-}
-
-void thread_cond_wait(thread_cond_t* cond, thread_lock_t* lock)
-{
- assert(cond);
- assert(lock);
- ABORTFAIL(pthread_cond_wait(&cond->cond, &lock->mutex));
-}
-
-bool thread_cond_timedwait(thread_cond_t* cond, thread_lock_t* lock,
- const struct timespec *abstime)
-{
- int ret;
- assert(cond);
- assert(lock);
- assert(abstime);
- ret = pthread_cond_timedwait(&cond->cond, &lock->mutex, abstime);
- if (ret == 0)
- {
- return true;
- }
-#ifdef DEBUG
- if (ret != ETIMEDOUT)
- {
- abort();
- }
-#endif
- return false;
-}
-
-void thread_cond_signal(thread_cond_t* cond)
-{
- assert(cond);
- ABORTFAIL(pthread_cond_signal(&cond->cond));
-}
-
-void thread_cond_broadcast(thread_cond_t* cond)
-{
- ABORTFAIL(pthread_cond_broadcast(&cond->cond));
-}
-
-void thread_abstime(struct timespec *abstime, unsigned long add_ms)
-{
-#if HAVE_CLOCK_GETTIME
- clock_gettime(CLOCK_REALTIME, abstime);
-#else
- {
- struct timeval tv;
- gettimeofday(&tv, NULL);
- abstime->tv_sec = tv.tv_sec;
- abstime->tv_nsec = tv.tv_usec * 1000;
- }
-#endif
- timespec_addms(abstime, add_ms);
-}
-
-struct thread_data_t
-{
- pthread_key_t key;
-};
-
-thread_data_t *thread_data_new(thread_data_value_free_t free_value)
-{
- thread_data_t *data = malloc(sizeof(thread_data_t));
- if (data)
- {
- if (pthread_key_create(&data->key, free_value))
- {
- free(data);
- return NULL;
- }
- }
- return data;
-}
-
-void thread_data_free(thread_data_t *data)
-{
- if (data)
- {
- pthread_key_delete(data->key);
- free(data);
- }
-}
-
-void *thread_data_value(thread_data_t *data)
-{
- assert(data);
- return pthread_getspecific(data->key);
-}
-
-void thread_data_set(thread_data_t *data, void *value)
-{
- assert(data);
- ABORTFAIL(pthread_setspecific(data->key, value));
-}
-
-void thread_once(thread_once_t *once, thread_run_once_t run)
-{
- ABORTFAIL(pthread_once(once, run));
-}
diff --git a/src/thread.h b/src/thread.h
deleted file mode 100644
index c0a21b0..0000000
--- a/src/thread.h
+++ /dev/null
@@ -1,266 +0,0 @@
-/**
- * \file thread.h
- * A collection of thread methods.
- */
-
-#ifndef THREAD_H
-#define THREAD_H
-
-#include "timespec.h"
-
-#if HAVE_PTHREAD
-#include <pthread.h>
-/**
- * Opaque type defining an once variable.
- */
-typedef pthread_once_t thread_once_t;
-/**
- * Init value for an once variable
- */
-#define THREAD_ONCE_INIT PTHREAD_ONCE_INIT
-#else
-# error missing thread_once_t implementation
-#endif
-
-/**
- * Opaque type defining a joinable thread.
- */
-typedef struct thread_t thread_t;
-/**
- * Opaque type defining an id.
- */
-typedef struct thread_id_t thread_id_t;
-/**
- * Opaque type defining a lock.
- */
-typedef struct thread_lock_t thread_lock_t;
-/**
- * Opaque type defining a conditional.
- */
-typedef struct thread_cond_t thread_cond_t;
-/**
- * Opaque type defining a thread local variable.
- */
-typedef struct thread_data_t thread_data_t;
-
-/**
- * Signature or function type for a thread run method.
- * A thread run method is the thread main() if you will.
- * When the method returns the thread is terminated.
- * @param userdata argument given when starting the thread
- */
-typedef void* (* thread_run_t)(void* userdata);
-
-/**
- * Create and start a thread.
- * @param run thread run method
- * @param userdata argument to run method, may not be NULL
- * @return true if thread was started without errors, may be NULL
- */
-NONULL_ARGS(1)
-bool thread_new(thread_run_t run, void* userdata);
-
-/**
- * Create and start a joinable thread.
- * Must call thread_join to free thread resources.
- * @param run thread run method, may not be NULL
- * @param userdata argument to run method, may be NULL
- * @return NULL in case of error
- */
-NONULL_ARGS(1)
-MALLOC thread_t* thread_joinable_new(thread_run_t run, void* userdata);
-
-/**
- * Wait for a joinable thread to exit, free it and return the result.
- * @param thread result from call to thread_joinable_new, may not be NULL
- * @return result of the thread run method
- */
-NONULL void* thread_join(thread_t* thread);
-
-
-/**
- * Create a unique thread id pointing to the given or current thread.
- * If no thread is given then a id for the current running thread is returned.
- * Returns NULL in case of error.
- * @param thread result from call to thread_joinable_new or NULL
- * @return a new thread id
- */
-MALLOC thread_id_t* thread_id_new(thread_t* thread);
-
-/**
- * Free a thread id.
- * @param id thread id to free, may be NULL
- */
-void thread_id_free(thread_id_t* id);
-
-/**
- * Return true if the current thread matches the thread id.
- * @param id thread id to check, may not be NULL
- * @return true if the id matches the current thread
- */
-NONULL bool thread_id_is_current(thread_id_t* id);
-
-/**
- * Thread yield, give another thread the chance to run.
- */
-void thread_yield(void);
-
-/**
- * Create a possibly non-recursive lock (mutex).
- * Returns NULL in case of error.
- * @return a new thread lock
- */
-MALLOC thread_lock_t* thread_lock_new(void);
-
-/**
- * Create a recursive lock (mutex).
- * Returns NULL in case of error.
- * @return a new thread lock
- */
-MALLOC thread_lock_t* thread_lock_recursive_new(void);
-
-/**
- * Free a lock.
- * The lock must not have a owner when freed or undefined behaviour will ensue.
- * @param lock lock to free, may be NULL
- */
-void thread_lock_free(thread_lock_t* lock);
-
-/**
- * Take ownership of the lock.
- * If another thread has ownership of the lock, wait until that thread releases
- * the lock.
- * If the current thread already has ownership of the lock and the lock is
- * recursive the current thread now owns the lock twice.
- * Undefined behavior (abort or deadlock probably) if the lock isn't recursive
- * and the current thread already has ownership.
- * @param lock lock to take ownership of
- */
-NONULL void thread_lock_lock(thread_lock_t* lock);
-/**
- * Release ownership of the lock.
- * Only call if the current thread has one or more ownerships of the lock.
- * If the lock is recursive only one ownership is released.
- * Undefined behaviour (abort or deadlock probably) if the lock isn't owned
- * by the current thread.
- * @param lock lock to release
- */
-NONULL void thread_lock_unlock(thread_lock_t* lock);
-
-/**
- * Create a conditional. Conditionals can be used to signal other threads
- * that a condition has been fulfilled if they were waiting for it.
- * Returns NULL in case of error.
- * @return new conditional
- */
-MALLOC thread_cond_t* thread_cond_new(void);
-/**
- * Free a conditional. No thread may be waiting on the conditional or undefined
- * behaviour will ensue.
- * @param cond conditional to free, may be NULL
- */
-void thread_cond_free(thread_cond_t* cond);
-
-/**
- * Setup the current thread to wait for the condition to be signalled.
- * The lock must be owned by the current thread before calling and will be
- * release while the current thread is waiting but reclaimed upon return.
- * @param cond conditonal to wait upon, may not be NULL
- * @param lock lock to release and retake ownership of, may not be NULL
- */
-NONULL void thread_cond_wait(thread_cond_t* cond, thread_lock_t* lock);
-
-/**
- * Setup the current thread to wait for the condition to be signalled until
- * abstime.
- * The lock must be owned by the current thread before calling and will be
- * release while the current thread is waiting but reclaimed upon return.
- * Use thread_abstime() to fill out the abstime.
- * @param cond conditonal to wait upon, may not be NULL
- * @param lock lock to release and retake ownership of, may not be NULL
- * @param abstime time when to give up and return, may not be NULL
- * @return true if the condition was signalled, false if it timed out
- */
-NONULL bool thread_cond_timedwait(thread_cond_t* cond, thread_lock_t* lock,
- const struct timespec *abstime);
-
-/**
- * Signal one thread that is currently waiting on the given condition.
- * The selected thread (which one is random) will be woken up.
- * The calling thread may or may not own the lock when calling
- * thread_cond_signal but the signalled thread will not wake up before the lock
- * is released.
- * @param cond conditional to signal, may not be NULL
- */
-NONULL void thread_cond_signal(thread_cond_t* cond);
-
-/**
- * Signal all threads that is currently waiting on the given condition.
- * All waiting threads will be woken up but only one may get ownership
- * of the lock.
- * The calling thread may or may not own the lock when calling
- * thread_cond_broadcast but the signalled threads will not wake up before the
- * lock is released.
- * @param cond conditional to signal, may not be NULL
- */
-NONULL void thread_cond_broadcast(thread_cond_t* cond);
-
-/**
- * Get the current absolute time and add a certain number of milliseconds
- * to it. To be used with thread_cond_timedwait.
- * @param abstime struct to file with the current absolute time and the
- * extra milliseconds.
- * @param add_ms milliseconds to add to the current absolute time
- */
-NONULL void thread_abstime(struct timespec *abstime, unsigned long add_ms);
-
-/**
- * Function type for a thread local variable value destructor
- */
-typedef void (* thread_data_value_free_t)(void *value);
-
-/**
- * Create a thread local variable.
- * Returns NULL in case of error.
- * @param free_value destructor for set non-null value at thread exit,
- * may be NULL
- * @return new thread local variable or NULL
- */
-MALLOC thread_data_t *thread_data_new(thread_data_value_free_t free_value);
-
-/**
- * Free a thread local variable.
- * Does not call any destructor.
- * @param data thread local variable to free, may be NULL
- */
-void thread_data_free(thread_data_t *data);
-
-/**
- * Get the thread local value of the variable.
- * @param data thread local variable, may not be NULL
- * @return thread local value for variable
- */
-NONULL void *thread_data_value(thread_data_t *data);
-
-/**
- * Set thread local value for variable.
- * @param data thread local variable, may not be NULL
- * @param value new value, may be NULL
- */
-NONULL_ARGS(1)
-void thread_data_set(thread_data_t *data, void *value);
-
-/**
- * Function type for a thread once method
- */
-typedef void (* thread_run_once_t)(void);
-
-/**
- * The first thread calling this method will run "run". Any subsequent thread
- * calling thread_once for the same once variable will do nothing.
- * @param once address to a static once variable, may not be NULL
- * @param run method to run if this is the first thread, may not be NULL
- */
-NONULL void thread_once(thread_once_t *once, thread_run_once_t run);
-
-#endif /* THREAD_H */
diff --git a/src/timer.cc b/src/timer.cc
new file mode 100644
index 0000000..8c97d32
--- /dev/null
+++ b/src/timer.cc
@@ -0,0 +1,555 @@
+#include "common.hh"
+
+#include "args.hh"
+#include "timer_state.hh"
+#include "xcb_atoms.hh"
+#include "xcb_colors.hh"
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+#include "xcb_resource.hh"
+#include "xcb_resources.hh"
+#include "xcb_xkb.hh"
+#include "xdg.hh"
+
+#include <algorithm>
+#include <condition_variable>
+#include <deque>
+#include <errno.h>
+#include <iostream>
+#include <limits>
+#include <mutex>
+#include <optional>
+#include <string.h>
+#include <thread>
+#include <xcb/xcb_icccm.h>
+
+#ifndef VERSION
+# warning No version defined
+# define VERSION
+#endif
+
+namespace {
+
+constexpr char const kTitle[] = "Timer";
+constexpr char const kClass[] = "org.the_jk.timer";
+
+constexpr std::chrono::seconds const kRedrawInterval{36};
+
+class Timer : public TimerState::Delegate {
+public:
+ explicit Timer(std::filesystem::path state_file)
+ : state_file_(std::move(state_file)),
+ state_(TimerState::create(state_file_, this)) {}
+
+ bool good() const {
+ return state_ != nullptr;
+ }
+
+ bool run(Option const* display, std::optional<std::string> font_name) {
+ int screen_index = 0;
+ if (display->is_set()) {
+ conn_ = xcb::make_shared_conn(xcb_connect(display->arg().c_str(),
+ &screen_index));
+ } else {
+ conn_ = xcb::make_shared_conn(xcb_connect(nullptr, &screen_index));
+ }
+
+ {
+ auto err = xcb_connection_has_error(conn_.get());
+ if (err) {
+ std::cerr << "Unable to connect to X display: " << err << std::endl;
+ return false;
+ }
+ }
+
+ auto atoms = xcb::Atoms::create(conn_);
+ string_atom_ = atoms->get("STRING");
+ wm_protocols_ = atoms->get("WM_PROTOCOLS");
+ wm_delete_window_ = atoms->get("WM_DELETE_WINDOW");
+
+ auto* screen = xcb::get_screen(conn_.get(), screen_index);
+ assert(screen);
+
+ auto colors = xcb::Colors::create(conn_, screen->default_colormap);
+ background_inactive_ =
+ colors->get_with_fallback(0x80, 0x80, 0x80, screen->white_pixel);
+ foreground_inactive_ =
+ colors->get_with_fallback(0x00, 0x00, 0x00, screen->black_pixel);
+ background_active_ =
+ colors->get_with_fallback(0x80, 0xff, 0x80, screen->white_pixel);
+ foreground_active_ =
+ colors->get_with_fallback(0x00, 0x00, 0x00, screen->black_pixel);
+
+ keyboard_ = xcb::Keyboard::create(conn_.get());
+ if (!keyboard_) {
+ std::cerr << "Failed to initialize XKB." << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ wnd_ = xcb::make_unique_wnd(conn_);
+ gc_ = xcb::make_unique_gc(conn_);
+ font_ = xcb::make_unique_font(conn_);
+
+ if (!font_name) {
+ auto resources = xcb::Resources::create(conn_);
+ font_name = resources->get_string("timer.font", "");
+ if (!font_name) {
+ font_name = "fixed";
+ }
+ }
+
+ auto font_cookie = xcb_open_font_checked(conn_.get(), font_->id(),
+ font_name->size(),
+ font_name->data());
+
+ if (!atoms->sync()) {
+ std::cerr << "Failed to get X atoms." << std::endl;
+ return EXIT_FAILURE;
+ }
+ if (!colors->sync()) {
+ std::cerr << "Failed to get X colors." << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ uint32_t value_list[3];
+ uint32_t value_mask = 0;
+ value_mask |= XCB_CW_BACK_PIXEL;
+ value_list[0] = screen->black_pixel;
+ value_mask |= XCB_CW_EVENT_MASK;
+ value_list[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS |
+ XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
+ xcb_create_window(conn_.get(), XCB_COPY_FROM_PARENT, wnd_->id(),
+ screen->root, 0, 0, wnd_width_, wnd_height_, 0,
+ XCB_WINDOW_CLASS_INPUT_OUTPUT,
+ screen->root_visual, value_mask, value_list);
+
+ xcb_icccm_set_wm_name(conn_.get(), wnd_->id(), string_atom_->get(),
+ 8, sizeof(kTitle) - 1, kTitle);
+ xcb_icccm_set_wm_class(conn_.get(), wnd_->id(), sizeof(kClass) - 1, kClass);
+ xcb_atom_t atom_list[1];
+ atom_list[0] = wm_delete_window_->get();
+ xcb_icccm_set_wm_protocols(conn_.get(), wnd_->id(),
+ wm_protocols_->get(), 1, atom_list);
+
+ value_mask = XCB_GC_FONT;
+ value_list[0] = font_->id();
+ xcb_create_gc(conn_.get(), gc_->id(), wnd_->id(), value_mask, value_list);
+
+ {
+ xcb::generic_error error(xcb_request_check(conn_.get(), font_cookie));
+ if (error) {
+ std::cerr << "Failed to load font " << *font_name << ": "
+ << static_cast<int>(error->error_code) << std::endl;
+ return false;
+ }
+ }
+
+ update_text();
+
+ xcb_map_window(conn_.get(), wnd_->id());
+ xcb_flush(conn_.get());
+
+ std::thread xcb_thread(&Timer::run_xcb, this);
+
+ bool quit = false;
+ std::chrono::steady_clock::time_point next_redraw;
+ if (active_) {
+ next_redraw = std::chrono::steady_clock::now() + kRedrawInterval;
+ }
+ while (!quit) {
+ ActionData action;
+ {
+ std::unique_lock<std::mutex> lock(action_lock_);
+ while (action_.empty()) {
+ if (active_) {
+ if (action_cond_.wait_until(lock, next_redraw)
+ == std::cv_status::timeout) {
+ next_redraw = std::chrono::steady_clock::now() + kRedrawInterval;
+ update_text();
+ draw();
+ }
+ } else {
+ action_cond_.wait(lock);
+ }
+ }
+ action = action_.front();
+ action_.pop_front();
+ }
+
+ switch (action.action) {
+ case Action::QUIT:
+ quit = true;
+ break;
+ case Action::DRAW:
+ if (action.b) {
+ draw();
+ }
+ break;
+ case Action::RESIZE:
+ wnd_width_ = action.rect.width;
+ wnd_height_ = action.rect.height;
+ break;
+ case Action::TOGGLE:
+ if (active_) {
+ state_->stop();
+ } else {
+ state_->start();
+ }
+ break;
+ case Action::SEND_RESET:
+ state_->reset();
+ break;
+ case Action::RECV_RESET:
+ total_ = std::chrono::minutes::zero();
+ update_text();
+ draw();
+ break;
+ case Action::START:
+ active_ = true;
+ next_redraw = std::chrono::steady_clock::now() + kRedrawInterval;
+ total_ = action.duration;
+ epoch_ = action.point;
+ update_text();
+ draw();
+ break;
+ case Action::STOP:
+ active_ = false;
+ total_ = action.duration;
+ update_text();
+ draw();
+ break;
+ case Action::RESTART:
+ state_ = TimerState::create(state_file_, this);
+ if (!state_) {
+ std::cerr << "Timer state failed and unable to restart." << std::endl;
+ quit = true;
+ conn_.reset();
+ break;
+ }
+ }
+ }
+
+ xcb_thread.join();
+ return true;
+ }
+
+ void start(
+ std::chrono::minutes total,
+ std::chrono::time_point<std::chrono::system_clock> epoch) override {
+ post(Action::START, total, epoch);
+ }
+
+ void stop(std::chrono::minutes total) override {
+ post(Action::STOP, total);
+ }
+
+ void reset() override {
+ post(Action::RECV_RESET);
+ }
+
+ void restart() override {
+ post(Action::RESTART);
+ }
+
+private:
+ enum class Action {
+ QUIT,
+ DRAW,
+ RESIZE,
+ TOGGLE,
+ START,
+ STOP,
+ SEND_RESET,
+ RECV_RESET,
+ RESTART,
+ };
+
+ struct ActionData {
+ Action action;
+ xcb_rectangle_t rect;
+ bool b{false};
+ std::chrono::minutes duration;
+ std::chrono::time_point<std::chrono::system_clock> point;
+
+ ActionData() = default;
+
+ explicit ActionData(Action action)
+ : action(action) {
+ assert(action == Action::QUIT || action == Action::TOGGLE ||
+ action == Action::SEND_RESET || action == Action::RECV_RESET ||
+ action == Action::RESTART);
+ }
+
+ ActionData(Action action, xcb_rectangle_t rect, bool last)
+ : action(action), rect(rect), b(last) {
+ assert(action == Action::DRAW);
+ }
+
+ ActionData(Action action, uint16_t width, uint16_t height)
+ : action(action), rect({0, 0, width, height}) {
+ assert(action == Action::RESIZE);
+ }
+
+ ActionData(Action action, std::chrono::minutes total,
+ std::chrono::time_point<std::chrono::system_clock> epoch)
+ : action(action), duration(total), point(epoch) {
+ assert(action == Action::START);
+ }
+
+ ActionData(Action action, std::chrono::minutes total)
+ : action(action), duration(total) {
+ assert(action == Action::STOP);
+ }
+ };
+
+ void run_xcb() {
+ while (true) {
+ xcb::generic_event event(xcb_wait_for_event(conn_.get()));
+ if (!event) {
+ auto err = xcb_connection_has_error(conn_.get());
+ if (err) {
+ std::cerr << "X connection had fatal error: " << err << std::endl;
+ } else {
+ std::cerr << "X connection had fatal I/O error." << std::endl;
+ }
+ break;
+ }
+ auto response_type = XCB_EVENT_RESPONSE_TYPE(event.get());
+ if (response_type == XCB_EXPOSE) {
+ auto* e = reinterpret_cast<xcb_expose_event_t*>(event.get());
+ if (e->window == wnd_->id()) {
+ xcb_rectangle_t rect;
+ rect.x = e->x;
+ rect.y = e->y;
+ rect.width = e->width;
+ rect.height = e->height;
+ post(Action::DRAW, rect, e->count == 0);
+ }
+ continue;
+ } else if (response_type == XCB_KEY_PRESS) {
+ auto* e = reinterpret_cast<xcb_key_press_event_t*>(event.get());
+ if (e->event == wnd_->id()) {
+ auto str = keyboard_->get_utf8(e);
+ if (str == "q" || str == "\x1b" /* Escape */) {
+ break;
+ } else if (str == " ") {
+ post(Action::TOGGLE);
+ } else if ((e->state & XCB_MOD_MASK_CONTROL) &&
+ str == "\x12" /* Ctrl + R */) {
+ post(Action::SEND_RESET);
+ }
+ }
+ continue;
+ } else if (response_type == XCB_BUTTON_PRESS) {
+ auto* e = reinterpret_cast<xcb_button_press_event_t*>(event.get());
+ if (e->event == wnd_->id()) {
+ if (e->detail == 1 /* Left button */) {
+ post(Action::TOGGLE);
+ }
+ }
+ continue;
+ } else if (response_type == XCB_CONFIGURE_NOTIFY) {
+ auto* e = reinterpret_cast<xcb_configure_notify_event_t*>(event.get());
+ if (e->window == wnd_->id()) {
+ post(Action::RESIZE, e->width, e->height);
+ }
+ continue;
+ } else if (response_type == XCB_REPARENT_NOTIFY) {
+ // Ignored, part of XCB_EVENT_MASK_STRUCTURE_NOTIFY
+ continue;
+ } else if (response_type == XCB_MAP_NOTIFY) {
+ // Ignored, part of XCB_EVENT_MASK_STRUCTURE_NOTIFY
+ continue;
+ } else if (keyboard_->handle_event(conn_.get(), event.get())) {
+ continue;
+ } else if (response_type == XCB_CLIENT_MESSAGE) {
+ auto* e = reinterpret_cast<xcb_client_message_event_t*>(event.get());
+ if (e->window == wnd_->id() && e->type == wm_protocols_->get() &&
+ e->format == 32) {
+ if (e->data.data32[0] == wm_delete_window_->get()) {
+ break;
+ }
+ }
+ continue;
+ }
+
+#ifndef NDEBUG
+ if (response_type == 0) {
+ auto* e = reinterpret_cast<xcb_generic_error_t*>(event.get());
+ std::cout << "Unhandled error: "
+ << xcb_event_get_error_label(e->error_code) << std::endl;
+ } else {
+ std::cout << "Unhandled event: " << xcb_event_get_label(response_type)
+ << std::endl;
+ }
+#endif
+ }
+
+ post(Action::QUIT);
+ }
+
+ template<class... Args>
+ void post(Args&&... args) {
+ bool notify;
+ {
+ std::lock_guard<std::mutex> lock(action_lock_);
+ notify = action_.empty();
+ action_.emplace_back(std::forward<Args>(args)...);
+ }
+ if (notify)
+ action_cond_.notify_one();
+ }
+
+ void update_text() {
+ char tmp[50];
+ int len;
+
+ using hours = std::chrono::duration<float, std::ratio<60 * 60>>;
+
+ if (active_) {
+ auto diff = std::chrono::duration_cast<std::chrono::minutes>(
+ std::chrono::system_clock::now() - epoch_);
+
+ len = snprintf(tmp, sizeof(tmp), "%.2f (%.2f)", hours(diff).count(),
+ hours(total_ + diff).count());
+ } else {
+ len = snprintf(tmp, sizeof(tmp), "(%.2f)", hours(total_).count());
+ }
+ if (len < 0 || len == sizeof(tmp))
+ return;
+
+ text_.resize(len);
+ for (int i = 0; i < len; ++i) {
+ text_[i].byte1 = 0;
+ text_[i].byte2 = tmp[i];
+ }
+
+ text_extents_cookie_ = xcb_query_text_extents(conn_.get(), font_->id(),
+ text_.size(), text_.data());
+ text_extents_width_ = 0;
+ }
+
+ void draw() {
+ xcb_rectangle_t r{0, 0, wnd_width_, wnd_height_};
+
+ uint32_t values[2];
+ values[0] =
+ active_ ? background_active_->get() : background_inactive_->get();
+ xcb_change_gc(conn_.get(), gc_->id(), XCB_GC_FOREGROUND, values);
+ xcb_poly_fill_rectangle(conn_.get(), wnd_->id(), gc_->id(), 1, &r);
+
+ values[0] =
+ active_ ? foreground_active_->get() : foreground_inactive_->get();
+ values[1] =
+ active_ ? background_active_->get() : background_inactive_->get();
+ xcb_change_gc(conn_.get(), gc_->id(),
+ XCB_GC_FOREGROUND | XCB_GC_BACKGROUND, values);
+
+ if (text_extents_width_ == 0) {
+ xcb::reply<xcb_query_text_extents_reply_t> reply(
+ xcb_query_text_extents_reply(conn_.get(), text_extents_cookie_,
+ nullptr));
+ if (reply) {
+ text_extents_width_ = reply->overall_width;
+ text_extents_height_ = reply->font_ascent;
+ } else {
+ text_extents_width_ = 1;
+ text_extents_height_ = 0;
+ }
+ }
+
+ auto x = (text_extents_width_ < wnd_width_) ?
+ (wnd_width_ - text_extents_width_) / 2 : 0;
+ auto y = wnd_height_ / 2 + text_extents_height_ / 2;
+
+ xcb_image_text_16(conn_.get(), text_.size(), wnd_->id(), gc_->id(), x, y,
+ text_.data());
+
+ xcb_flush(conn_.get());
+ }
+
+ xcb::shared_conn conn_;
+ std::optional<xcb::Atoms::Reference> string_atom_;
+ std::optional<xcb::Atoms::Reference> wm_protocols_;
+ std::optional<xcb::Atoms::Reference> wm_delete_window_;
+ std::optional<xcb::Colors::Color> background_inactive_;
+ std::optional<xcb::Colors::Color> background_active_;
+ std::optional<xcb::Colors::Color> foreground_inactive_;
+ std::optional<xcb::Colors::Color> foreground_active_;
+ std::unique_ptr<xcb::Keyboard> keyboard_;
+ xcb::unique_wnd wnd_;
+ xcb::unique_gc gc_;
+ xcb::unique_font font_;
+ uint16_t wnd_width_{100};
+ uint16_t wnd_height_{50};
+
+ bool active_{false};
+ std::chrono::minutes total_{0};
+ std::chrono::time_point<std::chrono::system_clock> epoch_;
+
+ std::vector<xcb_char2b_t> text_;
+ xcb_query_text_extents_cookie_t text_extents_cookie_;
+ uint16_t text_extents_width_{0};
+ uint16_t text_extents_height_{0};
+
+ std::mutex action_lock_;
+ std::condition_variable action_cond_;
+ std::deque<ActionData> action_;
+
+ std::filesystem::path const state_file_;
+ std::unique_ptr<TimerState> state_;
+};
+
+} // namespace
+
+int main(int argc, char** argv) {
+ auto args = Args::create();
+ auto* help = args->add_option('h', "help", "display this text and exit.");
+ auto* version = args->add_option('V', "version", "display version and exit.");
+ auto* opt_state_file = args->add_option_with_arg(
+ 'S', "state", "load state from FILE instead of default.", "FILE");
+ auto* opt_font_name = args->add_option_with_arg(
+ 'F', "font", "use font named FONT instead of default or Xresources.",
+ "FONT");
+ auto* display = args->add_option_with_arg(
+ 'D', "display", "connect to DISPLAY instead of default.", "DISPLAY");
+ std::vector<std::string> arguments;
+ if (!args->run(argc, argv, "timer", std::cerr, &arguments)) {
+ std::cerr << "Try `timer --help` for usage." << std::endl;
+ return EXIT_FAILURE;
+ }
+ if (help->is_set()) {
+ std::cout << "Usage: `timer [OPTIONS]`\n"
+ << "Timer is a timekeeping tool.\n"
+ << "\n";
+ args->print_descriptions(std::cout, 80);
+ return EXIT_SUCCESS;
+ }
+ if (version->is_set()) {
+ std::cout << "Timer " VERSION " written by "
+ << "Joel Klinghed <the_jk@spawned.biz>" << std::endl;
+ std::cout << "Icon by Free Preloaders [https://freeicons.io/profile/726]"
+ << " on https://freeicons.io" << std::endl;
+ return EXIT_SUCCESS;
+ }
+ if (!arguments.empty()) {
+ std::cerr << "Unexpected arguments after options.\n"
+ << "Try `timer --help` for usage." << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ std::filesystem::path state_file;
+ if (opt_state_file->is_set()) {
+ state_file = opt_state_file->arg();
+ } else {
+ state_file = xdg::path_to_write(xdg::Type::DATA, "timer.state");
+ }
+
+ Timer timer(state_file);
+ if (!timer.good())
+ return EXIT_FAILURE;
+ return timer.run(display,
+ opt_font_name->is_set() ? opt_font_name->arg() :
+ std::optional<std::string>())
+ ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/src/timer_state.cc b/src/timer_state.cc
new file mode 100644
index 0000000..770bbad
--- /dev/null
+++ b/src/timer_state.cc
@@ -0,0 +1,419 @@
+#include "common.hh"
+
+#include "io.hh"
+#include "timer_state.hh"
+#include "unique_fd.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <functional>
+#include <iostream>
+#include <sdbus-c++/sdbus-c++.h>
+#include <string.h>
+#include <sys/file.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <thread>
+
+namespace {
+
+constexpr char kServiceName[] = "org.the_jk.timer";
+constexpr char kObjectPath[] = "/org/the_jk/timer/state";
+constexpr char kInterfaceName[] = "org.the_jk.timer.State";
+
+class TimerStateImpl {
+public:
+ virtual ~TimerStateImpl() = default;
+
+ virtual void start() = 0;
+ virtual void stop() = 0;
+ virtual void reset() = 0;
+
+ virtual void enterLoop() {
+ conn_->enterEventLoop();
+ }
+
+ virtual void leaveLoop() {
+ conn_->leaveEventLoop();
+ }
+
+protected:
+ TimerStateImpl(std::shared_ptr<sdbus::IConnection> conn,
+ TimerState::Delegate* delegate)
+ : conn_(std::move(conn)), delegate_(delegate) {
+ }
+
+ std::shared_ptr<sdbus::IConnection> conn_;
+ TimerState::Delegate* const delegate_;
+};
+
+class TimerStateClient : public TimerStateImpl {
+public:
+ TimerStateClient(std::shared_ptr<sdbus::IConnection> conn,
+ TimerState::Delegate* delegate)
+ : TimerStateImpl(std::move(conn), delegate) {}
+
+ void start() override {
+ try {
+ proxy_->callMethod("start").onInterface(kInterfaceName).dontExpectReply();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to call start: " << err.what() << std::endl;
+ }
+ }
+
+ void stop() override {
+ try {
+ proxy_->callMethod("stop").onInterface(kInterfaceName).dontExpectReply();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to call stop: " << err.what() << std::endl;
+ }
+ }
+
+ void reset() override {
+ try {
+ proxy_->callMethod("reset").onInterface(kInterfaceName).dontExpectReply();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to call reset: " << err.what() << std::endl;
+ }
+ }
+
+ bool init() {
+ try {
+ proxy_ = sdbus::createProxy(*conn_.get(), kServiceName, kObjectPath);
+ proxy_->uponSignal("started").onInterface(kInterfaceName)
+ .call([this](uint32_t total, time_t epoch){
+ signal_started(std::chrono::minutes(total),
+ std::chrono::system_clock::from_time_t(epoch));
+ });
+ proxy_->uponSignal("stopped").onInterface(kInterfaceName)
+ .call([this](uint32_t total){
+ signal_stopped(std::chrono::minutes(total));
+ });
+ proxy_->uponSignal("reset").onInterface(kInterfaceName)
+ .call([this](){ signal_reset(); });
+ proxy_->finishRegistration();
+
+ dbus_proxy_ = sdbus::createProxy(*conn_.get(), "org.freedesktop.DBus",
+ "/org/freedesktop/DBus");
+ dbus_proxy_->uponSignal("NameOwnerChanged")
+ .onInterface("org.freedesktop.DBus")
+ .call([this](const std::string& name,
+ const std::string& /* old_owner */,
+ const std::string& new_owner) {
+ if (name == kServiceName && new_owner.empty()) {
+ signal_restart();
+ }
+ });
+ dbus_proxy_->finishRegistration();
+
+ sync_state();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to init client: " << err.what() << std::endl;
+ return false;
+ }
+
+ return true;
+ }
+
+private:
+ void signal_started(
+ std::chrono::minutes total,
+ std::chrono::time_point<std::chrono::system_clock> epoch) {
+ delegate_->start(total, epoch);
+ }
+
+ void signal_stopped(std::chrono::minutes total) {
+ delegate_->stop(total);
+ }
+
+ void signal_reset() {
+ delegate_->reset();
+ }
+
+ void signal_restart() {
+ delegate_->restart();
+ }
+
+ void sync_state() {
+ auto method = proxy_->createMethodCall(kInterfaceName, "get_state");
+ auto reply = proxy_->callMethod(std::move(method));
+ bool active;
+ uint32_t total;
+ time_t epoch;
+ reply >> active;
+ reply >> total;
+ reply >> epoch;
+ if (active) {
+ delegate_->start(std::chrono::minutes(total),
+ std::chrono::system_clock::from_time_t(epoch));
+ } else {
+ delegate_->stop(std::chrono::minutes(total));
+ }
+ }
+
+ std::unique_ptr<sdbus::IProxy> proxy_;
+ std::unique_ptr<sdbus::IProxy> dbus_proxy_;
+};
+
+class TimerStateServer : public TimerStateImpl {
+public:
+ TimerStateServer(std::shared_ptr<sdbus::IConnection> conn,
+ TimerState::Delegate* delegate)
+ : TimerStateImpl(std::move(conn), delegate) {}
+
+ void start() override {
+ if (active_) return;
+ active_ = true;
+ start_ = std::chrono::system_clock::now();
+ write_state();
+
+ try {
+ object_->emitSignal("started").onInterface(kInterfaceName).withArguments(
+ total_.count(), std::chrono::system_clock::to_time_t(start_));
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to emit started: " << err.what() << std::endl;
+ }
+ delegate_->start(total_, start_);
+ }
+
+ void stop() override {
+ if (!active_) return;
+ active_ = false;
+ total_ +=
+ std::chrono::duration_cast<std::chrono::minutes>(
+ std::chrono::system_clock::now() - start_);
+ write_state();
+
+ try {
+ object_->emitSignal("stopped").onInterface(kInterfaceName).withArguments(
+ total_.count());
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to emit started: " << err.what() << std::endl;
+ }
+ delegate_->stop(total_);
+ }
+
+ void reset() override {
+ if (active_) return;
+ total_ = std::chrono::minutes::zero();
+ write_state();
+
+ try {
+ object_->emitSignal("reset").onInterface(kInterfaceName);
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to emit reset: " << err.what() << std::endl;
+ }
+ delegate_->reset();
+ }
+
+ bool init(std::filesystem::path state_file) {
+ if (!load_state(state_file))
+ return false;
+
+ try {
+ auto object = sdbus::createObject(*conn_.get(), kObjectPath);
+ std::function<void()> fun = std::bind(&TimerStateServer::start, this);
+ object->registerMethod("start").onInterface(kInterfaceName)
+ .implementedAs(fun).withNoReply();
+ fun = std::bind(&TimerStateServer::stop, this);
+ object->registerMethod("stop").onInterface(kInterfaceName)
+ .implementedAs(fun).withNoReply();
+ fun = std::bind(&TimerStateServer::reset, this);
+ object->registerMethod("reset").onInterface(kInterfaceName)
+ .implementedAs(fun).withNoReply();
+ std::function<void(sdbus::MethodCall)> call_fun =
+ std::bind(&TimerStateServer::get_state, this, std::placeholders::_1);
+ object->registerMethod(kInterfaceName, "get_state", "", "bdd", call_fun);
+ object->registerSignal("started").onInterface(kInterfaceName)
+ .withParameters<uint32_t, time_t>();
+ object->registerSignal("stopped").onInterface(kInterfaceName)
+ .withParameters<uint32_t>();
+ object->registerSignal("reset").onInterface(kInterfaceName);
+ object->finishRegistration();
+
+ object_ = std::move(object);
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to init server: " << err.what() << std::endl;
+ return false;
+ }
+
+ if (active_) {
+ delegate_->start(total_, start_);
+ } else {
+ delegate_->stop(total_);
+ }
+
+ return true;
+ }
+
+private:
+ void get_state(sdbus::MethodCall call) {
+ try {
+ auto reply = call.createReply();
+ reply << active_;
+ reply << static_cast<uint32_t>(total_.count());
+ if (active_) {
+ reply << std::chrono::system_clock::to_time_t(start_);
+ } else {
+ reply << static_cast<time_t>(0);
+ }
+ reply.send();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to reply to get_state: " << err.what() << std::endl;
+ }
+ }
+
+ bool load_state(std::filesystem::path state_file) {
+ fd_.reset(open(state_file.c_str(), O_RDWR | O_CREAT, S_IRWXU));
+ if (!fd_) {
+ std::cerr << "Unable to open or create " << state_file
+ << " for reading and writing." << std::endl;
+ return false;
+ }
+ if (flock(fd_.get(), LOCK_EX | LOCK_NB)) {
+ std::cerr << "Unable to get exclusive lock on " << state_file
+ << ": " << strerror(errno) << std::endl;
+ return false;
+ }
+ std::string data;
+ if (!io::read_all(fd_.get(), &data)) {
+ std::cerr << "Error reading " << state_file
+ << ": " << strerror(errno) << std::endl;
+ return false;
+ }
+ if (!parse_state(std::move(data))) {
+ std::cerr << "Invalid data in state " << state_file << "." << std::endl;
+ return false;
+ }
+ return true;
+ }
+
+ bool parse_state(std::string data) {
+ try {
+ size_t end;
+ auto active = std::stol(data, &end);
+ if (end == data.size() || data[end] != '|')
+ return false;
+ data = data.substr(end + 1);
+ auto total = std::stoul(data, &end);
+ if (end == data.size() || data[end] != '|')
+ return false;
+ struct tm tm;
+ auto* endp = strptime(
+ data.substr(end + 1).c_str(), "%Y-%m-%d %H:%M:%S", &tm);
+ if (!endp || (*endp != '\0' && *endp != '\n'))
+ return false;
+
+ active_ = active == 1;
+ total_ = std::chrono::minutes(total);
+ start_ = std::chrono::system_clock::from_time_t(timegm(&tm));
+
+ return true;
+ } catch (std::exception const& e) {
+ return false;
+ }
+ }
+
+ bool write_state() {
+ std::string data =
+ std::to_string(active_ ? 1L : -1L) + '|' + std::to_string(
+ std::chrono::duration_cast<std::chrono::minutes>(total_).count())
+ + '|';
+ char tmp[50];
+ auto time = std::chrono::system_clock::to_time_t(start_);
+ auto len = strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S", gmtime(&time));
+ if (len == 0 || len == sizeof(tmp)) {
+ std::cerr << "Failed to store state: invalid time." << std::endl;
+ return false;
+ }
+ data.append(tmp, len);
+ data.push_back('\n');
+ if (lseek(fd_.get(), 0, SEEK_SET) ||
+ !io::write_all(fd_.get(), data) ||
+ ftruncate(fd_.get(), data.size())) {
+ std::cerr << "Failed to store state: " << strerror(errno) << std::endl;
+ return false;
+ }
+ return true;
+ }
+
+ bool active_{false};
+ std::chrono::minutes total_{0};
+ std::chrono::time_point<std::chrono::system_clock> start_;
+ std::unique_ptr<sdbus::IObject> object_;
+ unique_fd fd_;
+};
+
+class TimerStateWrapper : public TimerState {
+public:
+ explicit TimerStateWrapper(Delegate* delegate)
+ : delegate_(delegate) {}
+
+ ~TimerStateWrapper() override {
+ if (impl_) impl_->leaveLoop();
+ thread_.join();
+ }
+
+ bool init(std::filesystem::path state_file) {
+ try {
+ std::shared_ptr<sdbus::IConnection> conn(
+ sdbus::createSessionBusConnection());
+
+ try {
+ conn->requestName(kServiceName);
+
+ auto server = std::make_unique<TimerStateServer>(conn, delegate_);
+ if (server->init(std::move(state_file))) {
+ impl_ = std::move(server);
+ return post_init();
+ }
+ // If server fails to init in any way, try client as backup.
+ } catch (sdbus::Error const& e) {
+ // This is here to catch requestName call, if requestName fails there
+ // is a server running.
+ }
+
+ auto client = std::make_unique<TimerStateClient>(conn, delegate_);
+ if (client->init()) {
+ impl_ = std::move(client);
+ return post_init();
+ }
+ } catch (sdbus::Error const& e) {
+ }
+ return false;
+ }
+
+ void start() override {
+ impl_->start();
+ }
+
+ void stop() override {
+ impl_->stop();
+ }
+
+ void reset() override {
+ impl_->reset();
+ }
+
+private:
+ bool post_init() {
+ thread_ = std::thread(&TimerStateWrapper::run_impl, this);
+ return true;
+ }
+
+ void run_impl() {
+ impl_->enterLoop();
+ }
+
+ Delegate* const delegate_;
+ std::unique_ptr<TimerStateImpl> impl_;
+ std::thread thread_;
+};
+
+} // namespace
+
+std::unique_ptr<TimerState> TimerState::create(std::filesystem::path state_file,
+ Delegate* delegate) {
+ auto state = std::make_unique<TimerStateWrapper>(delegate);
+ return state->init(std::move(state_file)) ? std::move(state) : nullptr;
+}
diff --git a/src/timer_state.hh b/src/timer_state.hh
new file mode 100644
index 0000000..0a3c1d1
--- /dev/null
+++ b/src/timer_state.hh
@@ -0,0 +1,64 @@
+#ifndef TIMER_STATE_HH
+#define TIMER_STATE_HH
+
+#include <chrono>
+#include <filesystem>
+#include <memory>
+
+class TimerState {
+public:
+ virtual ~TimerState() = default;
+
+ /**
+ * Delegate methods must be thread-safe, they will be called on
+ * a thread owned by TimerState.
+ */
+ class Delegate {
+ public:
+ virtual ~Delegate() = default;
+
+ virtual void start(
+ std::chrono::minutes total,
+ std::chrono::time_point<std::chrono::system_clock> epoch) = 0;
+
+ virtual void stop(std::chrono::minutes total) = 0;
+
+ virtual void reset() = 0;
+
+ virtual void restart() = 0;
+
+ protected:
+ Delegate() = default;
+ };
+
+ /**
+ * Start timer. If already started nothing happens.
+ * Method must be called on the same thread as create.
+ */
+ virtual void start() = 0;
+ /**
+ * Stop timer. If already stopped nothing happens.
+ * Method must be called on the same thread as create.
+ */
+ virtual void stop() = 0;
+ /**
+ * Clear total if stopped. If started nothing happens.
+ * Method must be called on the same thread as create.
+ */
+ virtual void reset() = 0;
+
+ /**
+ * Sets up a shared TimerState. state is stored in state_file.
+ * Note that delegate may be called before method returns.
+ * Returns nullptr in case of error.
+ */
+ static std::unique_ptr<TimerState> create(std::filesystem::path state_file,
+ Delegate* delegate);
+
+protected:
+ TimerState() = default;
+ TimerState(TimerState const&) = delete;
+ TimerState& operator=(TimerState const&) = delete;
+};
+
+#endif // TIMER_STATE_HH
diff --git a/src/timespec.c b/src/timespec.c
deleted file mode 100644
index 533f00d..0000000
--- a/src/timespec.c
+++ /dev/null
@@ -1,280 +0,0 @@
-#include "common.h"
-
-#include "timespec.h"
-
-#if !HAVE_CLOCK_GETTIME
-# include <sys/time.h>
-#endif
-
-#if HAVE_CLOCK_GETTIME
-static bool support_monotonic = true;
-#elif defined(__MACH__)
-#include <mach/mach_time.h>
-static struct mach_timebase_info timebase_info = { 0, 0 };
-
-/*
- * The methods below to muldiv128 have the listed copyright:
- *
- * Copyright (c) 1999, 2003, 2006, 2007 Apple Inc. All rights reserved.
- *
- * This file contains Original Code and/or Modifications of Original Code
- * as defined in and that are subject to the Apple Public Source License
- * Version 2.0 (the 'License'). You may not use this file except in
- * compliance with the License. Please obtain a copy of the License at
- * http://www.opensource.apple.com/apsl/ and read it before using this
- * file.
- *
- * The Original Code and all software distributed under the License are
- * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
- * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
- * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
- * Please see the License for the specific language governing rights and
- * limitations under the License.
- */
-
-typedef struct
-{
- uint64_t high;
- uint64_t low;
-} uint128_t;
-
-/* acc += add */
-static inline void add128_128(uint128_t *acc, uint128_t *add)
-{
- acc->high += add->high;
- acc->low += add->low;
- if (acc->low < add->low)
- {
- acc->high++; // carry
- }
-}
-
-/* acc -= sub */
-static inline void sub128_128(uint128_t *acc, uint128_t *sub)
-{
- acc->high -= sub->high;
- if (acc->low < sub->low)
- {
- acc->high--; // borrow
- }
- acc->low -= sub->low;
-}
-
-static inline double uint128_double(uint128_t *u)
-{
- return (((double)(1ULL << 32)) * ((double)(1ULL << 32))) *
- u->high + u->low; // may loses precision
- }
-
-/* 64x64 -> 128 bit multiplication */
-static inline void mul64x64(uint64_t x, uint64_t y, uint128_t *prod)
-{
- uint128_t add;
- /*
- * Split the two 64-bit multiplicands into 32-bit parts:
- * x => 2^32 * x1 + x2
- * y => 2^32 * y1 + y2
- */
- uint32_t x1 = (uint32_t)(x >> 32);
- uint32_t x2 = (uint32_t)x;
- uint32_t y1 = (uint32_t)(y >> 32);
- uint32_t y2 = (uint32_t)y;
- /*
- * direct multiplication:
- * x * y => 2^64 * (x1 * y1) + 2^32 (x1 * y2 + x2 * y1) + (x2 * y2)
- * The first and last terms are direct assignmenet into the uint128_t
- * structure. Then we add the middle two terms separately, to avoid
- * 64-bit overflow. (We could use the Karatsuba algorithm to save
- * one multiply, but it is harder to deal with 64-bit overflows.)
- */
- prod->high = (uint64_t)x1 * (uint64_t)y1;
- prod->low = (uint64_t)x2 * (uint64_t)y2;
- add.low = (uint64_t)x1 * (uint64_t)y2;
- add.high = (add.low >> 32);
- add.low <<= 32;
- add128_128(prod, &add);
- add.low = (uint64_t)x2 * (uint64_t)y1;
- add.high = (add.low >> 32);
- add.low <<= 32;
- add128_128(prod, &add);
-}
-
-/* (x * y / divisor) */
-static uint64_t muldiv128(uint64_t x, uint64_t y, uint64_t divisor)
-{
- uint128_t temp;
- uint128_t divisor128 = {0, divisor};
- uint64_t result = 0;
- double recip;
-
- mul64x64(x, y, &temp);
-
- /*
- * Now divide by the divisor. We use floating point to calculate an
- * approximate answer and update the results. Then we iterate and
- * calculate a correction from the difference.
- */
- recip = 1.0 / (double)divisor;
- while (temp.high || temp.low >= divisor)
- {
- uint128_t backmul;
- uint64_t uapprox;
-
- uapprox = (uint64_t)(uint128_double(&temp) * recip);
- mul64x64(uapprox, divisor, &backmul);
- /*
- * Because we are using unsigned integers, we need to approach the
- * answer from the lesser side. So if our estimate is too large
- * we need to decrease it until it is smaller.
- */
- while (backmul.high > temp.high ||
- (backmul.high == temp.high && backmul.low > temp.low))
- {
- sub128_128(&backmul, &divisor128);
- uapprox--;
- }
- sub128_128(&temp, &backmul);
- result += uapprox;
- }
- return result;
-}
-#endif
-
-void timespec_now(struct timespec *ts)
-{
- assert(ts);
-#if HAVE_CLOCK_GETTIME
- if (support_monotonic)
- {
- if (clock_gettime(CLOCK_MONOTONIC, ts) == 0)
- {
- return;
- }
- support_monotonic = false;
- }
- clock_gettime(CLOCK_REALTIME, ts);
-#elif defined( __MACH__)
- if (timebase_info.denom == 0)
- {
- mach_timebase_info(&timebase_info);
- }
- {
- uint64_t time;
- if (timebase_info.denom == timebase_info.numer)
- {
- time = mach_absolute_time();
- }
- else
- {
- time = muldiv128(timebase_info.numer, mach_absolute_time(),
- timebase_info.denom);
- }
- ts->tv_sec = time / 1000000000;
- ts->tv_nsec = time % 1000000000;
- }
-#else
- {
- struct timeval tv;
- gettimeofday(&tv, NULL);
- ts->tv_sec = tv.tv_sec;
- ts->tv_nsec = tv.tv_usec * 1000;
- }
-#endif
-}
-
-void timespec_addms(struct timespec *ts, unsigned long ms)
-{
- const unsigned int sec = ms / 1000;
- assert(ts);
- ms -= sec * 1000;
- ts->tv_nsec += ms * 1000000;
- ts->tv_sec += ts->tv_nsec / 1000000000 + sec;
- ts->tv_nsec = ts->tv_nsec % 1000000000;
-}
-
-void timespec_add(struct timespec *ts, const struct timespec *add)
-{
- ts->tv_nsec += add->tv_nsec;
- ts->tv_sec += ts->tv_nsec / 1000000000 + add->tv_sec;
- ts->tv_nsec = ts->tv_nsec % 1000000000;
-}
-
-int timespec_sub(struct timespec *ts, const struct timespec *sub)
-{
- if (ts->tv_sec < sub->tv_sec)
- {
- ts->tv_sec = sub->tv_sec - ts->tv_sec;
- if (ts->tv_nsec <= sub->tv_nsec)
- {
- ts->tv_nsec = sub->tv_nsec - ts->tv_nsec;
- }
- else
- {
- ts->tv_sec--;
- ts->tv_nsec = 1000000000 + sub->tv_nsec - ts->tv_nsec;
- }
- return -1;
- }
- else if (ts->tv_sec > sub->tv_sec)
- {
- ts->tv_sec -= sub->tv_sec;
- if (ts->tv_nsec < sub->tv_nsec)
- {
- ts->tv_sec--;
- ts->tv_nsec = 1000000000 + ts->tv_nsec - sub->tv_nsec;
- }
- else
- {
- ts->tv_nsec -= sub->tv_nsec;
- }
- return 1;
- }
- else /* if (ts->tv_sec == sub->tv_sec) */
- {
- ts->tv_sec = 0;
- if (ts->tv_nsec < sub->tv_nsec)
- {
- ts->tv_nsec = sub->tv_nsec - ts->tv_nsec;
- return -1;
- }
- else if (ts->tv_nsec > sub->tv_nsec)
- {
- ts->tv_nsec -= sub->tv_nsec;
- return 1;
- }
- else
- {
- ts->tv_nsec = 0;
- return 0;
- }
- }
-}
-
-int timespec_cmp(const struct timespec *x, const struct timespec *y)
-{
- if (x->tv_sec < y->tv_sec)
- {
- return -1;
- }
- else if (x->tv_sec > y->tv_sec)
- {
- return 1;
- }
- else /* if (x->tv_sec == y->tv_sec) */
- {
- if (x->tv_nsec < y->tv_nsec)
- {
- return -1;
- }
- else if (x->tv_nsec > y->tv_nsec)
- {
- return 1;
- }
- else
- {
- return 0;
- }
- }
-}
-
diff --git a/src/timespec.h b/src/timespec.h
deleted file mode 100644
index 2bf1182..0000000
--- a/src/timespec.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * \file timespec.h
- * Declares timespec struct if platform is missing it
- */
-
-#ifndef TIMESPEC_H
-#define TIMESPEC_H
-
-#include <time.h>
-#if !HAVE_STRUCT_TIMESPEC
-struct timespec
-{
- time_t tv_sec;
- long tv_nsec;
-};
-#endif
-
-NONULL void timespec_now(struct timespec *ts);
-
-NONULL void timespec_addms(struct timespec *ts, unsigned long ms);
-NONULL void timespec_add(struct timespec *ts, const struct timespec *add);
-
-NONULL int timespec_sub(struct timespec *ts, const struct timespec *sub);
-NONULL int timespec_cmp(const struct timespec *x, const struct timespec *y);
-
-#endif /* TIMESPEC_H */
diff --git a/src/unique_fd.cc b/src/unique_fd.cc
new file mode 100644
index 0000000..a289f0e
--- /dev/null
+++ b/src/unique_fd.cc
@@ -0,0 +1,17 @@
+#include "common.hh"
+
+#include "unique_fd.hh"
+
+#include <unistd.h>
+
+void unique_fd::reset(int fd) {
+ if (fd_ >= 0)
+ close(fd_);
+ fd_ = fd;
+}
+
+unique_fd unique_fd::dup() {
+ if (fd_ >= 0)
+ return unique_fd(::dup(fd_));
+ return unique_fd();
+}
diff --git a/src/unique_fd.hh b/src/unique_fd.hh
new file mode 100644
index 0000000..d795d68
--- /dev/null
+++ b/src/unique_fd.hh
@@ -0,0 +1,50 @@
+#ifndef UNIQUE_FD_HH
+#define UNIQUE_FD_HH
+
+#include <cstddef>
+
+class unique_fd {
+public:
+ constexpr unique_fd() noexcept
+ : fd_(-1) {}
+ constexpr unique_fd(std::nullptr_t) noexcept
+ : fd_(-1) {}
+ explicit unique_fd(int fd) noexcept
+ : fd_(fd) {}
+ unique_fd(unique_fd&& fd) noexcept
+ : fd_(fd.release()) {}
+
+ ~unique_fd() { reset(); }
+
+ unique_fd& operator=(unique_fd&& fd) noexcept {
+ reset(fd.release());
+ return *this;
+ }
+ unique_fd& operator=(std::nullptr_t) noexcept {
+ reset();
+ return *this;
+ }
+
+ int get() const noexcept { return fd_; }
+ int operator*() const { return get(); }
+
+ explicit operator bool() const noexcept { return fd_ >= 0; }
+
+ int release() noexcept {
+ int ret = fd_;
+ fd_ = -1;
+ return ret;
+ }
+
+ void reset(int fd = -1);
+
+ unique_fd dup();
+
+private:
+ unique_fd(unique_fd const&) = delete;
+ unique_fd& operator=(unique_fd const&) = delete;
+
+ int fd_;
+};
+
+#endif // UNIQUE_FD_HH
diff --git a/src/xcb_atoms.cc b/src/xcb_atoms.cc
new file mode 100644
index 0000000..300b859
--- /dev/null
+++ b/src/xcb_atoms.cc
@@ -0,0 +1,75 @@
+#include "common.hh"
+
+#include "xcb_atoms.hh"
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+
+#include <map>
+#include <vector>
+
+namespace xcb {
+
+namespace {
+
+class AtomsImpl : public Atoms {
+public:
+ explicit AtomsImpl(shared_conn conn)
+ : conn_(conn), storage_(std::make_shared<StorageImpl>()) {}
+
+ Reference get(std::string atom) override {
+ auto it = index_.find(atom);
+ size_t index;
+ if (it == index_.end()) {
+ index = cookie_.size();
+ cookie_.push_back(
+ xcb_intern_atom(conn_.get(), 0, atom.size(), atom.c_str()));
+ index_.emplace(std::move(atom), index);
+ } else {
+ index = it->second;
+ }
+ return Reference(storage_, index);
+ }
+
+ bool sync() override {
+ std::vector<xcb_atom_t> atoms;
+ atoms.reserve(cookie_.size());
+ for (auto const& cookie : cookie_) {
+ xcb::reply<xcb_intern_atom_reply_t> reply(
+ xcb_intern_atom_reply(conn_.get(), cookie, nullptr));
+ if (!reply)
+ return false;
+ atoms.push_back(reply->atom);
+ }
+ storage_->set(std::move(atoms));
+ return true;
+ }
+
+private:
+ class StorageImpl : public Storage {
+ public:
+ xcb_atom_t get(size_t id) const override {
+ assert(id < resolved_.size());
+ return resolved_[id];
+ }
+
+ void set(std::vector<xcb_atom_t> resolved) {
+ resolved_ = std::move(resolved);
+ }
+
+ private:
+ std::vector<xcb_atom_t> resolved_;
+ };
+
+ shared_conn conn_;
+ std::map<std::string, size_t> index_;
+ std::vector<xcb_intern_atom_cookie_t> cookie_;
+ std::shared_ptr<StorageImpl> storage_;
+};
+
+} // namespace
+
+std::unique_ptr<Atoms> Atoms::create(shared_conn conn) {
+ return std::make_unique<AtomsImpl>(conn);
+}
+
+} // namespace xcb
diff --git a/src/xcb_atoms.hh b/src/xcb_atoms.hh
new file mode 100644
index 0000000..a0f136e
--- /dev/null
+++ b/src/xcb_atoms.hh
@@ -0,0 +1,57 @@
+#ifndef XCB_ATOMS_HH
+#define XCB_ATOMS_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+#include <string>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+class Atoms {
+protected:
+ class Storage;
+
+public:
+ virtual ~Atoms() = default;
+
+ class Reference {
+ public:
+ xcb_atom_t get() {
+ return atoms_->get(id_);
+ }
+
+ Reference(std::shared_ptr<Storage> atoms, size_t id)
+ : atoms_(atoms), id_(id) {}
+
+ private:
+ std::shared_ptr<Storage> atoms_;
+ size_t id_;
+ };
+
+ virtual Reference get(std::string atom) = 0;
+
+ virtual bool sync() = 0;
+
+ static std::unique_ptr<Atoms> create(shared_conn conn);
+
+protected:
+ Atoms() = default;
+ Atoms(Atoms const&) = delete;
+ Atoms& operator=(Atoms const&) = delete;
+
+ class Storage {
+ public:
+ virtual ~Storage() = default;
+
+ virtual xcb_atom_t get(size_t id) const = 0;
+
+ protected:
+ Storage() = default;
+ };
+};
+
+} // namespace xcb
+
+#endif // XCB_ATOMS_HH
diff --git a/src/xcb_colors.cc b/src/xcb_colors.cc
new file mode 100644
index 0000000..f4caca4
--- /dev/null
+++ b/src/xcb_colors.cc
@@ -0,0 +1,93 @@
+#include "common.hh"
+
+#include "xcb_colors.hh"
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+
+#include <map>
+#include <vector>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+namespace {
+
+class ColorsImpl : public Colors {
+public:
+ ColorsImpl(shared_conn conn, xcb_colormap_t colormap)
+ : conn_(conn), colormap_(colormap),
+ storage_(std::make_shared<StorageImpl>()) {}
+
+ Color get_with_fallback(uint8_t r, uint8_t g, uint8_t b,
+ uint32_t fallback) override {
+ auto key = make_key(r, g, b);
+ auto it = index_.find(key);
+ size_t index;
+ if (it == index_.end()) {
+ index = cookie_.size();
+ cookie_.push_back(xcb_alloc_color(conn_.get(), colormap_,
+ static_cast<uint16_t>(r) << 8,
+ static_cast<uint16_t>(g) << 8,
+ static_cast<uint16_t>(b) << 8));
+ fallback_.push_back(fallback);
+ index_.emplace(key, index);
+ } else {
+ index = it->second;
+ }
+ return Color(storage_, index);
+ }
+
+ bool sync() override {
+ std::vector<uint32_t> colors;
+ colors.reserve(cookie_.size());
+ for (size_t i = 0; i < cookie_.size(); ++i) {
+ xcb::reply<xcb_alloc_color_reply_t> reply(
+ xcb_alloc_color_reply(conn_.get(), cookie_[i], nullptr));
+ if (reply) {
+ colors.push_back(reply->pixel);
+ } else {
+ colors.push_back(fallback_[i]);
+ }
+ }
+ storage_->set(std::move(colors));
+ return true;
+ }
+
+private:
+ class StorageImpl : public Storage {
+ public:
+ uint32_t get(size_t id) const override {
+ assert(id < resolved_.size());
+ return resolved_[id];
+ }
+
+ void set(std::vector<uint32_t> resolved) {
+ resolved_ = std::move(resolved);
+ }
+
+ private:
+ std::vector<uint32_t> resolved_;
+ };
+
+ static uint32_t make_key(uint8_t r, uint8_t g, uint8_t b) {
+ return static_cast<uint32_t>(r) << 16 |
+ static_cast<uint32_t>(g) << 8 | b;
+ }
+
+ shared_conn conn_;
+ xcb_colormap_t colormap_;
+ std::map<uint32_t, size_t> index_;
+ std::vector<xcb_alloc_color_cookie_t> cookie_;
+ std::vector<uint32_t> fallback_;
+ std::shared_ptr<StorageImpl> storage_;
+};
+
+} // namespace
+
+std::unique_ptr<Colors> Colors::create(shared_conn conn,
+ xcb_colormap_t colormap) {
+ return std::make_unique<ColorsImpl>(conn, colormap);
+}
+
+} // namespace xcb
+
diff --git a/src/xcb_colors.hh b/src/xcb_colors.hh
new file mode 100644
index 0000000..4db2b93
--- /dev/null
+++ b/src/xcb_colors.hh
@@ -0,0 +1,57 @@
+#ifndef XCB_COLORS_HH
+#define XCB_COLORS_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+
+namespace xcb {
+
+class Colors {
+protected:
+ class Storage;
+
+public:
+ virtual ~Colors() = default;
+
+ class Color {
+ public:
+ uint32_t get() {
+ return colors_->get(id_);
+ }
+
+ Color(std::shared_ptr<Storage> colors, size_t id)
+ : colors_(colors), id_(id) {}
+
+ private:
+ std::shared_ptr<Storage> colors_;
+ size_t id_;
+ };
+
+ virtual Color get_with_fallback(uint8_t r, uint8_t g, uint8_t b,
+ uint32_t fallback) = 0;
+
+ virtual bool sync() = 0;
+
+ static std::unique_ptr<Colors> create(shared_conn conn,
+ xcb_colormap_t colormap);
+
+protected:
+ Colors() = default;
+ Colors(Colors const&) = delete;
+ Colors& operator=(Colors const&) = delete;
+
+ class Storage {
+ public:
+ virtual ~Storage() = default;
+
+ virtual uint32_t get(size_t id) const = 0;
+
+ protected:
+ Storage() = default;
+ };
+};
+
+} // namespace xcb
+
+#endif // XCB_COLORS_HH
diff --git a/src/xcb_connection.cc b/src/xcb_connection.cc
new file mode 100644
index 0000000..66a5a1a
--- /dev/null
+++ b/src/xcb_connection.cc
@@ -0,0 +1,26 @@
+#include "common.hh"
+
+#include "xcb_connection.hh"
+
+namespace xcb {
+
+shared_conn make_shared_conn(xcb_connection_t* conn) {
+ return std::shared_ptr<xcb_connection_t>(
+ conn, internal::xcb_connection_deleter());
+}
+
+unique_conn make_unique_conn(xcb_connection_t* conn) {
+ return unique_conn(conn);
+}
+
+xcb_screen_t* get_screen(xcb_connection_t* conn, int screen_index) {
+ auto iter = xcb_setup_roots_iterator(xcb_get_setup(conn));
+ for (; iter.rem; --screen_index, xcb_screen_next(&iter)) {
+ if (screen_index == 0) {
+ return iter.data;
+ }
+ }
+ return nullptr;
+}
+
+} // namespace xcb
diff --git a/src/xcb_connection.hh b/src/xcb_connection.hh
new file mode 100644
index 0000000..74ee751
--- /dev/null
+++ b/src/xcb_connection.hh
@@ -0,0 +1,30 @@
+#ifndef XCB_CONNECTION_HH
+#define XCB_CONNECTION_HH
+
+#include <memory>
+#include <xcb/xcb.h>
+
+namespace xcb {
+
+namespace internal {
+
+struct xcb_connection_deleter {
+ void operator() (xcb_connection_t* ptr) {
+ xcb_disconnect(ptr);
+ }
+};
+
+} // namespace internal
+
+typedef std::shared_ptr<xcb_connection_t> shared_conn;
+typedef std::unique_ptr<xcb_connection_t,
+ internal::xcb_connection_deleter> unique_conn;
+
+shared_conn make_shared_conn(xcb_connection_t* conn);
+unique_conn make_unique_conn(xcb_connection_t* conn);
+
+xcb_screen_t* get_screen(xcb_connection_t* conn, int screen_index);
+
+} // namespace xcb
+
+#endif // XCB_CONNECTION_HH
diff --git a/src/xcb_event.hh b/src/xcb_event.hh
new file mode 100644
index 0000000..5934603
--- /dev/null
+++ b/src/xcb_event.hh
@@ -0,0 +1,32 @@
+#ifndef XCB_EVENT_HH
+#define XCB_EVENT_HH
+
+#include <memory>
+#include <stdlib.h>
+#include <xcb/xcb.h>
+#include <xcb/xcb_event.h>
+
+namespace xcb {
+
+namespace internal {
+
+struct FreeDeleter {
+ void operator() (void* ptr) {
+ free(ptr);
+ }
+};
+
+} // namespace internal
+
+typedef std::unique_ptr<xcb_generic_event_t,
+ internal::FreeDeleter> generic_event;
+
+typedef std::unique_ptr<xcb_generic_error_t,
+ internal::FreeDeleter> generic_error;
+
+template<typename T>
+using reply = std::unique_ptr<T, internal::FreeDeleter>;
+
+} // namespace xcb
+
+#endif // XCB_EVENT_HH
diff --git a/src/xcb_resource.cc b/src/xcb_resource.cc
new file mode 100644
index 0000000..b64b611
--- /dev/null
+++ b/src/xcb_resource.cc
@@ -0,0 +1,37 @@
+#include "common.hh"
+
+#include "xcb_resource.hh"
+
+namespace xcb {
+
+unique_wnd make_unique_wnd(shared_conn conn) {
+ return std::make_unique<xcb_resource<xcb_window_t,
+ internal::WndDeleter>>(conn);
+}
+
+shared_wnd make_shared_wnd(shared_conn conn) {
+ return std::make_shared<xcb_resource<xcb_window_t,
+ internal::WndDeleter>>(conn);
+}
+
+unique_gc make_unique_gc(shared_conn conn) {
+ return std::make_unique<xcb_resource<xcb_gcontext_t,
+ internal::GCDeleter>>(conn);
+}
+
+shared_gc make_shared_gc(shared_conn conn) {
+ return std::make_shared<xcb_resource<xcb_gcontext_t,
+ internal::GCDeleter>>(conn);
+}
+
+unique_font make_unique_font(shared_conn conn) {
+ return std::make_unique<xcb_resource<xcb_font_t,
+ internal::FontDeleter>>(conn);
+}
+
+shared_font make_shared_font(shared_conn conn) {
+ return std::make_shared<xcb_resource<xcb_font_t,
+ internal::FontDeleter>>(conn);
+}
+
+} // namespace xcb
diff --git a/src/xcb_resource.hh b/src/xcb_resource.hh
new file mode 100644
index 0000000..15619e5
--- /dev/null
+++ b/src/xcb_resource.hh
@@ -0,0 +1,105 @@
+#ifndef XCB_RESOURCE_HH
+#define XCB_RESOURCE_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+namespace internal {
+
+struct WndDeleter {
+ void operator() (xcb_connection_t* conn, xcb_window_t wnd) const {
+ xcb_destroy_window(conn, wnd);
+ }
+};
+
+struct GCDeleter {
+ void operator() (xcb_connection_t* conn, xcb_gcontext_t gc) const {
+ xcb_free_gc(conn, gc);
+ }
+};
+
+struct FontDeleter {
+ void operator() (xcb_connection_t* conn, xcb_font_t font) const {
+ xcb_close_font(conn, font);
+ }
+};
+
+} // namespace internal
+
+template<typename T, typename Deleter>
+class xcb_resource {
+public:
+ explicit xcb_resource(shared_conn conn)
+ : conn_(conn), id_(xcb_generate_id(conn_.get())) {}
+ constexpr xcb_resource()
+ : id_(XCB_NONE) {}
+ xcb_resource(xcb_resource const& res) = delete;
+ xcb_resource(xcb_resource&& res)
+ : conn_(res.conn_), id_(res.release()) { }
+ ~xcb_resource() {
+ reset();
+ }
+
+ xcb_resource& operator=(xcb_resource const& res) = delete;
+ xcb_resource& operator=(xcb_resource&& res) {
+ reset();
+ conn_ = res.conn_;
+ id_ = res.release();
+ return *this;
+ }
+
+ T id() const {
+ return id_;
+ }
+
+ void reset() {
+ if (id_ == XCB_NONE)
+ return;
+ deleter_(conn_.get(), id_);
+ id_ = XCB_NONE;
+ }
+
+ T release() {
+ auto ret = id_;
+ id_ = XCB_NONE;
+ conn_.reset();
+ return ret;
+ }
+
+private:
+ shared_conn conn_;
+ T id_;
+ Deleter const deleter_{};
+};
+
+typedef std::unique_ptr<xcb_resource<xcb_window_t,
+ internal::WndDeleter>> unique_wnd;
+typedef std::shared_ptr<xcb_resource<xcb_window_t,
+ internal::WndDeleter>> shared_wnd;
+
+unique_wnd make_unique_wnd(shared_conn conn);
+shared_wnd make_shared_wnd(shared_conn conn);
+
+typedef std::unique_ptr<xcb_resource<xcb_gcontext_t,
+ internal::GCDeleter>> unique_gc;
+typedef std::shared_ptr<xcb_resource<xcb_gcontext_t,
+ internal::GCDeleter>> shared_gc;
+
+unique_gc make_unique_gc(shared_conn conn);
+shared_gc make_shared_gc(shared_conn conn);
+
+typedef std::unique_ptr<xcb_resource<xcb_font_t,
+ internal::FontDeleter>> unique_font;
+typedef std::shared_ptr<xcb_resource<xcb_font_t,
+ internal::FontDeleter>> shared_font;
+
+unique_font make_unique_font(shared_conn conn);
+shared_font make_shared_font(shared_conn conn);
+
+} // namespace xcb
+
+#endif // XCB_RESOURCE_HH
diff --git a/src/xcb_resources.hh b/src/xcb_resources.hh
new file mode 100644
index 0000000..5e04e26
--- /dev/null
+++ b/src/xcb_resources.hh
@@ -0,0 +1,35 @@
+#ifndef XCB_RESOURCES_HH
+#define XCB_RESOURCES_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+#include <optional>
+#include <string>
+
+namespace xcb {
+
+class Resources {
+public:
+ virtual ~Resources() = default;
+
+ virtual std::optional<std::string> get_string(std::string const& name,
+ std::string const& clazz) = 0;
+
+ virtual std::optional<long> get_long(std::string const& name,
+ std::string const& clazz) = 0;
+
+ virtual std::optional<bool> get_bool(std::string const& name,
+ std::string const& clazz) = 0;
+
+ static std::unique_ptr<Resources> create(shared_conn conn);
+
+protected:
+ Resources() = default;
+ Resources(Resources const&) = delete;
+ Resources& operator=(Resources const&) = delete;
+};
+
+} // namespace xcb
+
+#endif // XCB_RESOURCES_HH
diff --git a/src/xcb_resources_none.cc b/src/xcb_resources_none.cc
new file mode 100644
index 0000000..be37dd9
--- /dev/null
+++ b/src/xcb_resources_none.cc
@@ -0,0 +1,35 @@
+#include "common.hh"
+
+#include "xcb_resources.hh"
+
+namespace xcb {
+
+namespace {
+
+class ResourcesNone : public Resources {
+public:
+ ResourcesNone() = default;
+
+ std::optional<std::string> get_string(std::string const&,
+ std::string const&) override {
+ return std::optional<std::string>();
+ }
+
+ std::optional<long> get_long(std::string const&,
+ std::string const&) override {
+ return std::optional<long>();
+ }
+
+ std::optional<bool> get_bool(std::string const&,
+ std::string const&) override {
+ return std::optional<bool>();
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Resources> Resources::create(shared_conn) {
+ return std::make_unique<ResourcesNone>();
+}
+
+} // namespace xcb
diff --git a/src/xcb_resources_xrm.cc b/src/xcb_resources_xrm.cc
new file mode 100644
index 0000000..a9149af
--- /dev/null
+++ b/src/xcb_resources_xrm.cc
@@ -0,0 +1,78 @@
+#include "common.hh"
+
+#include "xcb_resources.hh"
+
+#include <xcb/xcb_xrm.h>
+
+namespace xcb {
+
+namespace {
+
+struct DatabaseDeleter {
+ void operator() (xcb_xrm_database_t* db) {
+ xcb_xrm_database_free(db);
+ }
+};
+
+class ResourcesXrm : public Resources {
+public:
+ explicit ResourcesXrm(shared_conn conn)
+ : conn_(conn), db_(xcb_xrm_database_from_default(conn_.get())) {
+ }
+
+ std::optional<std::string> get_string(std::string const& name,
+ std::string const& clazz) override {
+ if (db_) {
+ char* tmp;
+ if (xcb_xrm_resource_get_string(db_.get(),
+ std::string(name).c_str(),
+ std::string(clazz).c_str(),
+ &tmp) >= 0) {
+ std::string ret(tmp);
+ free(tmp);
+ return ret;
+ }
+ }
+ return std::optional<std::string>();
+ }
+
+ std::optional<long> get_long(std::string const& name,
+ std::string const& clazz) override {
+ if (db_) {
+ long tmp;
+ if (xcb_xrm_resource_get_long(db_.get(),
+ std::string(name).c_str(),
+ std::string(clazz).c_str(),
+ &tmp) >= 0) {
+ return tmp;
+ }
+ }
+ return std::optional<long>();
+ }
+
+ std::optional<bool> get_bool(std::string const& name,
+ std::string const& clazz) override {
+ if (db_) {
+ bool tmp;
+ if (xcb_xrm_resource_get_bool(db_.get(),
+ std::string(name).c_str(),
+ std::string(clazz).c_str(),
+ &tmp) >= 0) {
+ return tmp;
+ }
+ }
+ return std::optional<bool>();
+ }
+
+private:
+ shared_conn conn_;
+ std::unique_ptr<xcb_xrm_database_t, DatabaseDeleter> db_;
+};
+
+} // namespace
+
+std::unique_ptr<Resources> Resources::create(shared_conn conn) {
+ return std::make_unique<ResourcesXrm>(conn);
+}
+
+} // namespace xcb
diff --git a/src/xcb_xkb.cc b/src/xcb_xkb.cc
new file mode 100644
index 0000000..f50ffe1
--- /dev/null
+++ b/src/xcb_xkb.cc
@@ -0,0 +1,171 @@
+#include "common.hh"
+
+#include "xcb_event.hh"
+#include "xcb_xkb.hh"
+
+#define explicit dont_use_cxx_explicit
+#include <xcb/xkb.h>
+#undef explicit
+#include <xkbcommon/xkbcommon-x11.h>
+
+namespace xcb {
+
+namespace {
+
+struct KeymapDeleter {
+ void operator() (xkb_keymap* keymap) const {
+ if (keymap)
+ xkb_keymap_unref(keymap);
+ }
+};
+
+struct StateDeleter {
+ void operator() (xkb_state* state) const {
+ if (state)
+ xkb_state_unref(state);
+ }
+};
+
+struct ContextDeleter {
+ void operator() (xkb_context* context) const {
+ if (context)
+ xkb_context_unref(context);
+ }
+};
+
+class KeyboardImpl : public Keyboard {
+public:
+ KeyboardImpl() = default;
+
+ bool init(xcb_connection_t* conn) {
+ if (!xkb_x11_setup_xkb_extension(conn,
+ XKB_X11_MIN_MAJOR_XKB_VERSION,
+ XKB_X11_MIN_MINOR_XKB_VERSION,
+ XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS,
+ NULL, NULL, &first_xkb_event_, NULL))
+ return false;
+
+ ctx_.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS));
+ if (!ctx_)
+ return false;
+ device_id_ = xkb_x11_get_core_keyboard_device_id(conn);
+ if (device_id_ == -1)
+ return false;
+ if (!update_keymap(conn))
+ return false;
+ select_events(conn);
+ return true;
+ }
+
+ bool handle_event(xcb_connection_t* conn,
+ xcb_generic_event_t* event) override {
+ if (XCB_EVENT_RESPONSE_TYPE(event) == first_xkb_event_) {
+ auto* xkb_event = reinterpret_cast<xkb_generic_event_t*>(event);
+ if (xkb_event->deviceID == device_id_) {
+ switch (xkb_event->xkbType) {
+ case XCB_XKB_NEW_KEYBOARD_NOTIFY: {
+ auto* e =
+ reinterpret_cast<xcb_xkb_new_keyboard_notify_event_t*>(event);
+ if (e->changed & XCB_XKB_NKN_DETAIL_KEYCODES)
+ update_keymap(conn);
+ break;
+ }
+ case XCB_XKB_MAP_NOTIFY:
+ update_keymap(conn);
+ break;
+ case XCB_XKB_STATE_NOTIFY: {
+ auto* e =
+ reinterpret_cast<xcb_xkb_state_notify_event_t*>(event);
+ xkb_state_update_mask(state_.get(),
+ e->baseMods, e->latchedMods, e->lockedMods,
+ e->baseGroup, e->latchedGroup, e->lockedGroup);
+ break;
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ std::string get_utf8(xcb_key_press_event_t* event) override {
+ char tmp[16];
+ xkb_state_key_get_utf8(state_.get(), event->detail, tmp, sizeof(tmp));
+ return std::string(tmp);
+ }
+
+private:
+ struct xkb_generic_event_t {
+ uint8_t response_type;
+ uint8_t xkbType;
+ uint16_t sequence;
+ xcb_timestamp_t time;
+ uint8_t deviceID;
+ };
+
+ bool update_keymap(xcb_connection_t* conn) {
+ auto* keymap = xkb_x11_keymap_new_from_device(ctx_.get(), conn, device_id_,
+ XKB_KEYMAP_COMPILE_NO_FLAGS);
+ if (!keymap)
+ return false;
+ auto* state = xkb_x11_state_new_from_device(keymap, conn, device_id_);
+ if (!state) {
+ xkb_keymap_unref(keymap);
+ return false;
+ }
+ keymap_.reset(keymap);
+ state_.reset(state);
+ return true;
+ }
+
+ void select_events(xcb_connection_t *conn) {
+ static const uint16_t new_keyboard_details = XCB_XKB_NKN_DETAIL_KEYCODES;
+ static const uint16_t map_parts = XCB_XKB_MAP_PART_KEY_TYPES |
+ XCB_XKB_MAP_PART_KEY_SYMS |
+ XCB_XKB_MAP_PART_MODIFIER_MAP |
+ XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
+ XCB_XKB_MAP_PART_KEY_ACTIONS |
+ XCB_XKB_MAP_PART_VIRTUAL_MODS |
+ XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP;
+ static const uint16_t state_details = XCB_XKB_STATE_PART_MODIFIER_BASE |
+ XCB_XKB_STATE_PART_MODIFIER_LATCH |
+ XCB_XKB_STATE_PART_MODIFIER_LOCK |
+ XCB_XKB_STATE_PART_GROUP_BASE |
+ XCB_XKB_STATE_PART_GROUP_LATCH |
+ XCB_XKB_STATE_PART_GROUP_LOCK;
+
+ xcb_xkb_select_events_details_t details = {};
+ details.affectNewKeyboard = new_keyboard_details;
+ details.newKeyboardDetails = new_keyboard_details;
+ details.affectState = state_details;
+ details.stateDetails = state_details;
+
+ xcb_xkb_select_events_aux(conn,
+ device_id_,
+ XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
+ XCB_XKB_EVENT_TYPE_MAP_NOTIFY |
+ XCB_XKB_EVENT_TYPE_STATE_NOTIFY,
+ 0,
+ 0,
+ map_parts,
+ map_parts,
+ &details);
+ }
+
+ std::unique_ptr<xkb_context, ContextDeleter> ctx_;
+ std::unique_ptr<xkb_keymap, KeymapDeleter> keymap_;
+ std::unique_ptr<xkb_state, StateDeleter> state_;
+ uint8_t first_xkb_event_;
+ int32_t device_id_;
+};
+
+} // namespace
+
+std::unique_ptr<Keyboard> Keyboard::create(xcb_connection_t* conn) {
+ auto ret = std::make_unique<KeyboardImpl>();
+ if (ret->init(conn))
+ return ret;
+ return nullptr;
+}
+
+} // namespace xcb
diff --git a/src/xcb_xkb.hh b/src/xcb_xkb.hh
new file mode 100644
index 0000000..7bb3681
--- /dev/null
+++ b/src/xcb_xkb.hh
@@ -0,0 +1,30 @@
+#ifndef XCB_XKB_HH
+#define XCB_XKB_HH
+
+#include <memory>
+#include <string>
+#include <xcb/xcb.h>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+class Keyboard {
+public:
+ virtual ~Keyboard() = default;
+
+ virtual bool handle_event(xcb_connection_t* conn,
+ xcb_generic_event_t* event) = 0;
+
+ virtual std::string get_utf8(xcb_key_press_event_t* event) = 0;
+
+ static std::unique_ptr<Keyboard> create(xcb_connection_t* conn);
+
+protected:
+ Keyboard() = default;
+ Keyboard(Keyboard const&) = delete;
+ Keyboard& operator=(Keyboard const&) = delete;
+};
+
+} // namespace xcb
+
+#endif // XCB_XKB_HH
diff --git a/src/xdg.cc b/src/xdg.cc
new file mode 100644
index 0000000..b61b4ba
--- /dev/null
+++ b/src/xdg.cc
@@ -0,0 +1,116 @@
+#include "common.hh"
+
+#include "xdg.hh"
+
+#include <filesystem>
+#include <pwd.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+namespace xdg {
+
+namespace {
+
+bool valid(char const* path) {
+ return path && path[0] == '/';
+}
+
+bool valid(std::string const& path, size_t pos, size_t len) {
+ return len > 0 && path[pos] == '/';
+}
+
+std::filesystem::path get_home() {
+ auto* env = getenv("HOME");
+ if (valid(env))
+ return env;
+ auto* pwd = getpwuid(getuid());
+ if (pwd && valid(pwd->pw_dir))
+ return pwd->pw_dir;
+ return "/";
+}
+
+void get_paths(Type type, std::string* dirs, std::filesystem::path* home) {
+ char const* env;
+ switch (type) {
+ case Type::CONFIG:
+ if (dirs) {
+ env = getenv("XDG_CONFIG_DIRS");
+ if (valid(env)) {
+ dirs->assign(env);
+ } else {
+ dirs->assign("/etc/xdg");
+ }
+ }
+ if (home) {
+ env = getenv("XDG_CONFIG_HOME");
+ if (valid(env)) {
+ *home = env;
+ } else {
+ *home = get_home() / ".config";
+ }
+ }
+ break;
+ case Type::DATA:
+ if (dirs) {
+ env = getenv("XDG_DATA_DIRS");
+ if (valid(env)) {
+ dirs->assign(env);
+ } else {
+ dirs->assign("usr/local/share/:/usr/share/");
+ }
+ }
+ if (home) {
+ env = getenv("XDG_DATA_HOME");
+ if (valid(env)) {
+ *home = env;
+ } else {
+ *home = get_home() / ".local/share";
+ }
+ }
+ break;
+ case Type::CACHE:
+ if (dirs)
+ dirs->clear();
+ if (home) {
+ env = getenv("XDG_CACHE_HOME");
+ if (valid(env)) {
+ *home = env;
+ } else {
+ *home = get_home() / ".cache";
+ }
+ }
+ break;
+ }
+}
+
+} // namespace
+
+void paths_to_read(Type type, std::string_view name,
+ std::vector<std::filesystem::path>* out) {
+ std::string dirs;
+ std::filesystem::path home;
+ get_paths(type, &dirs, &home);
+ out->clear();
+ out->push_back(home / name);
+ size_t last = 0;
+ while (true) {
+ size_t next = dirs.find(':', last);
+ if (next == std::string::npos) {
+ if (valid(dirs, last, dirs.size() - last))
+ out->push_back(std::filesystem::path(dirs.substr(last)) / name);
+ break;
+ }
+ if (valid(dirs, last, next - last))
+ out->push_back(
+ std::filesystem::path(dirs.substr(last, next - last)) / name);
+ last = next + 1;
+ }
+}
+
+std::filesystem::path path_to_write(Type type, std::string_view name) {
+ std::filesystem::path home;
+ get_paths(type, nullptr, &home);
+ return home / name;
+}
+
+} // namespace xdg
diff --git a/src/xdg.hh b/src/xdg.hh
new file mode 100644
index 0000000..1c7e4f0
--- /dev/null
+++ b/src/xdg.hh
@@ -0,0 +1,23 @@
+#ifndef XDG_HH
+#define XDG_HH
+
+#include <filesystem>
+#include <string_view>
+#include <vector>
+
+namespace xdg {
+
+enum class Type {
+ CONFIG,
+ DATA,
+ CACHE,
+};
+
+void paths_to_read(Type type, std::string_view path,
+ std::vector<std::filesystem::path>* out);
+
+std::filesystem::path path_to_write(Type type, std::string_view path);
+
+} // namespace xdg
+
+#endif // XDG_HH