misc: start rewrite in C++

Thu, 26 Apr 2018 13:23:44 +0200

author
David Demelier <markand@malikania.fr>
date
Thu, 26 Apr 2018 13:23:44 +0200
changeset 20
370213df9449
parent 19
ea81d5b2c72e
child 21
38d927bed5c3

misc: start rewrite in C++

.hgignore file | annotate | diff | comparison | revisions
CMakeLists.txt file | annotate | diff | comparison | revisions
INSTALL.md file | annotate | diff | comparison | revisions
Makefile file | annotate | diff | comparison | revisions
TODO.md file | annotate | diff | comparison | revisions
bin/dmenu_bg file | annotate | diff | comparison | revisions
bin/dmenu_filesel file | annotate | diff | comparison | revisions
bin/dmenu_mpc file | annotate | diff | comparison | revisions
bin/dmenu_power file | annotate | diff | comparison | revisions
bin/dmenu_ssh file | annotate | diff | comparison | revisions
dmenu-background/CMakeLists.txt file | annotate | diff | comparison | revisions
dmenu-background/main.cpp file | annotate | diff | comparison | revisions
doc/dmenu_bg.md file | annotate | diff | comparison | revisions
doc/dmenu_filesel.md file | annotate | diff | comparison | revisions
doc/dmenu_mpc.md file | annotate | diff | comparison | revisions
doc/dmenu_power.md file | annotate | diff | comparison | revisions
doc/dmenu_ssh.md file | annotate | diff | comparison | revisions
doc/dmenutools.md file | annotate | diff | comparison | revisions
libdmenutools/CMakeLists.txt file | annotate | diff | comparison | revisions
libdmenutools/dmenu/dmenu.cpp file | annotate | diff | comparison | revisions
libdmenutools/dmenu/dmenu.hpp file | annotate | diff | comparison | revisions
libdmenutools/dmenu/ini.cpp file | annotate | diff | comparison | revisions
libdmenutools/dmenu/ini.hpp file | annotate | diff | comparison | revisions
libdmenutools/dmenu/xdg.hpp file | annotate | diff | comparison | revisions
libexec/dmenutools/dmenu.subr file | annotate | diff | comparison | revisions
--- a/.hgignore	Tue Jan 02 13:27:42 2018 +0100
+++ b/.hgignore	Thu Apr 26 13:23:44 2018 +0200
@@ -1,6 +1,13 @@
-# ctags & vim.
-tags
+# Build directory used in lots of documentation.
+^build
+
+# Qt Creator creates CMakeLists.txt.user.
+^CMakeLists\.txt\.user$
+
+# vim/emacs specific.
+^tags$
 \.swp$
+\.swo$
 
-# manuals.
-\.1$
+# macOS specific.
+\.DS_Store$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CMakeLists.txt	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,28 @@
+#
+# CMakeLists.txt -- CMake build system for dmenutools
+#
+# Copyright (c) 2018 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+cmake_minimum_required(VERSION 3.5)
+project(dmenutools)
+
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_CXX_STANDARD_REQUIRED On)
+
+find_package(Boost REQUIRED COMPONENTS filesystem system)
+
+add_subdirectory(libdmenutools)
+add_subdirectory(dmenu-background)
--- a/INSTALL.md	Tue Jan 02 13:27:42 2018 +0100
+++ b/INSTALL.md	Thu Apr 26 13:23:44 2018 +0200
@@ -6,19 +6,20 @@
 Requirements
 ------------
 
-  - POSIX compatible shell,
-  - Pandoc for the documentation.
+  - Boost,
+  - CMake.
+
+For `dmenu-background`:
+
+  - feh (by default, can be changed)
 
 Basic installation
 ------------------
 
-Add the **bin/** directory to your path or you can install the scripts using
-`make install` target as root.
+Invoke CMake, make and make install.
 
-An additional variable `PREFIX` can be set to specify the installation
-directory.
-
-Examples:
-
-	make install			# install to /usr/local/bin
-	make install PREFIX=/	# install to /bin
+    mkdir build
+    cd build
+    cmake .. -DCMAKE\_BUILD\_TYPE=Release
+    make
+    sudo make install
--- a/Makefile	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-#
-# Copyright (c) 2017-2018 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-PREFIX=/usr/local
-MANDIR=share/man
-DOCSRC=	doc/dmenu_bg.md		\
-	doc/dmenu_filesel.md	\
-	doc/dmenu_mpc.md	\
-	doc/dmenu_power.md	\
-	doc/dmenu_ssh.md	\
-	doc/dmenutools.md
-DOCOBJ=	${DOCSRC:.md=.1}
-
-all: docs
-
-%.1: %.md
-	@echo "  MAN $@"
-	@pandoc -s -f markdown -t man $< -o $@
-
-docs: ${DOCOBJ}
-
-install: ${DOCOBJ}
-	@install -D -m 0644 doc/dmenutools.1 ${PREFIX}/${MANDIR}/man1
-	@install -D -m 0644 libexec/dmenutools/dmenu.subr ${PREFIX}/libexec/dmenutools/dmenu.subr
-	@echo "  INSTALL dmenu_bg"
-	@install -D -m 0755 bin/dmenu_bg ${PREFIX}/bin
-	@install -D -m 0644 doc/dmenu_bg.1 ${PREFIX}/${MANDIR}/man1
-	@echo "  INSTALL dmenu_filesel"
-	@install -D -m 0755 bin/dmenu_filesel ${PREFIX}/bin
-	@install -D -m 0644 doc/dmenu_filesel.1 ${PREFIX}/${MANDIR}/man1
-	@echo "  INSTALL dmenu_mpc"
-	@install -D -m 0755 bin/dmenu_mpc ${PREFIX}/bin
-	@install -D -m 0644 doc/dmenu_mpc.1 ${PREFIX}/${MANDIR}/man1
-	@echo "  INSTALL dmenu_power"
-	@install -D -m 0755 bin/dmenu_power ${PREFIX}/bin
-	@install -D -m 0644 doc/dmenu_power.1 ${PREFIX}/${MANDIR}/man1
-	@echo "  INSTALL dmenu_ssh"
-	@install -D -m 0755 bin/dmenu_ssh ${PREFIX}/bin
-	@install -D -m 0644 doc/dmenu_ssh.1 ${PREFIX}/${MANDIR}/man1
-
-
-clean:
-	@rm -f ${DOCOBJ}
--- a/TODO.md	Tue Jan 02 13:27:42 2018 +0100
+++ b/TODO.md	Thu Apr 26 13:23:44 2018 +0200
@@ -3,17 +3,12 @@
 
 List of things to improve or add.
 
-filesel
--------
+libdmenutools
+-------------
 
-  - Avoid using temporary files for listing if possible.
+  - Create convenient and powerful `dmenu::browse` API.
 
-mpc
----
-
-  - Remove temporary file for error reporting.
+New tools
+---------
 
-dmenu.subr
-----------
-
-  - Make dmt_fatal able to read from stdin.
+  - `dmenu-mpc`
--- a/bin/dmenu_bg	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-#!/bin/sh
-#
-# dmenu_bg -- prompt for a background
-#
-# Copyright (c) 2017-2018 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-TOOL="bg"
-TOP=$(dirname "$(readlink -f "$0")")
-
-dmt_bg_usage()
-{
-    echo "usage: dmenu_bg [directory]" 1>&2;
-    exit 1
-}
-
-. ${TOP}/../libexec/dmenutools/dmenu.subr
-
-# Check for an optional directory.
-if [ $# -ge 1 ]; then
-    directory="$1"
-elif [ -n "${bg_directory}" ]; then
-    directory="${bg_directory}"
-else
-    directory="${XDG_PICTURES_DIR:-${HOME}}"
-fi
-
-# Use user bg_cmd if defined, otherwise find one.
-if [ -z "${bg_cmd}" ]; then
-    if command -v feh > /dev/null 2>&1; then
-        bg_cmd="feh --bg-scale"
-    elif command -v hsetroot > /dev/null 2>&1; then
-        bg_cmd="hsetroot -fill"
-    elif command -v fbsetbg > /dev/null 2>&1; then
-        bg_cmd="fbsetbg"
-    else
-        dmt_fatal "no background manager found"
-        # NOTREACHED
-    fi
-fi
-
-# Optional lines.
-lines=${bg_lines:-16}
-
-file=$(${TOP}/dmenu_filesel -t f -l ${lines} -p "(jpe?g|png|gif)" ${directory})
-
-if [ -n "${file}" ]; then
-    ${bg_cmd} "${file}"
-fi
-
-# vim: syntax=sh:
--- a/bin/dmenu_filesel	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-#!/bin/sh
-#
-# dmenu_filesel -- prompt for a file or directory
-#
-# Copyright (c) 2017-2018 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-TOOL="filesel"
-TOP=$(dirname "$(readlink -f "$0")")
-
-dmt_filesel_usage()
-{
-    echo "usage: dmenu_filesel [-t file|directory]" 1>&2;
-    exit 1
-}
-
-dmt_filesel_list()
-{
-	list="/tmp/dmenutools-$(id -u).filesel_list.txt"
-	result="/tmp/dmenutools-$(id -u).filesel_result.txt"
-
-    # Allow '.' only if traget must be directory.
-    if [ "${ftype}" = "directory" ]; then
-		echo "." > ${result}
-    fi
-
-	echo ".." >> ${result}
-
-	# Avoid basename error in case of empty directories.
-    if [ -n "$(ls -A "${root}")" ]; then
-		find "${root}" -mindepth 1 -maxdepth 1 > ${list}
-
-		# Update the result list.
-		cat ${list} | while read n; do
-			keep=1
-
-			# Check for optional pattern
-			if [ -f "$n" ] && [ -n "${pattern}" ] && ! (echo $n | egrep -q "${pattern}"); then
-				keep=0
-			fi
-
-			if [ ${keep} -eq 1 ]; then
-				echo $(basename "$n") >> ${result}
-			fi
-		done
-    fi
-
-	cat ${result} | sort
-	rm -f ${result} ${list}
-}
-
-ftype="file"
-lines="16"
-
-while getopts "il:t:p:" opt; do
-    case ${opt} in
-    l)
-        lines=${OPTARG}
-        ;;
-	p)
-		pattern="${OPTARG}"
-		;;
-    t)
-        if [ "${OPTARG}" = "f" ] || [ "${OPTARG}" = "file" ]; then
-            ftype="file"
-        elif [ "${OPTARG}" = "d" ] || [ "${OPTARG}" = "directory" ]; then
-            ftype="directory"
-        fi
-        ;;
-    *)
-        dmt_filesel_usage
-        # NOTREACHED
-    esac
-done
-
-shift $((OPTIND - 1))
-
-# Start from here.
-if [ $# -ge 1 ]; then
-    root="${1}/"
-else
-    root="/"
-fi
-
-while [ -z "${selection}" ]; do
-    entry="$(dmt_filesel_list | dmenu -p "${root}" -l ${lines})"
-
-    # Abort on empty selection.
-    if [ -z "${entry}" ]; then
-        exit 1
-    fi
-
-    if [ "${entry}" = ".." ]; then
-        parent="$(dirname "${root}")"
-
-        if [ "${parent}" = "/" ]; then
-            root="/"
-        else
-            root="${parent}/"
-        fi
-
-        continue;
-    fi
-
-    # If directory, goes deeper.
-    path="${root}${entry}"
-
-    if [ -d "${path}" ]; then
-        if [ -r "${path}" ]; then
-            root="${path}/"
-        else
-            continue
-        fi
-    else
-        selection="${path}"
-    fi
-done
-
-echo "${selection}"
--- a/bin/dmenu_mpc	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-#!/bin/sh
-#
-# dmenu_mpc -- convenient mpc module
-#
-# Copyright (c) 2017-2018 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-TOOL="filesel"
-TOP=$(dirname "$(readlink -f "$0")")
-
-. ${TOP}/../libexec/dmenutools/dmenu.subr
-
-if [ -z "${MPD_HOST}" ] && [ -n "${mpc_host}" ]; then
-	export MPD_HOST="${mpc_host}"
-fi
-if [ -z "${MPD_PORT}" ] && [ -n "${mpc_port}" ]; then
-	export MPD_PORT="${mpc_port}"
-fi
-
-#
-# Get the list of files or directories.
-#
-# Arguments:
-#	- base, the optional base component path.
-#
-dmt_mpc_list()
-{
-	path="/tmp/dmenutools-$(id -u).mpc_list.txt"
-
-	echo "." > "${path}"
-
-	if [ -n "$1" ]; then
-		echo ".." >> "${path}"
-	fi
-
-	list=$(mpc ls "$1" >> "${path}" 2> /dev/null)
-
-	if [ $? -eq 0 ]; then
-		cat ${path} | while read n; do
-			basename "$n"
-		done
-	fi
-
-	rm -f "${path}" > /dev/null 2>&1
-}
-
-#
-# Select a file using dmt_mpc_list in successive dmenu calls.
-#
-dmt_mpc_select()
-{
-	while [ -z "${selection}" ]; do
-		entry="$(dmt_mpc_list "${root}" | dmenu -p "${root}" -l 16)"
-
-		# Abort on empty selection.
-		if [ -z "${entry}" ]; then
-			exit 1
-		fi
-
-		if [ "${entry}" = ".." ]; then
-			parent="$(dirname "${root}")"
-
-			if [ "${parent}" = "." ]; then
-				root=""
-			else
-				root="${parent}"
-			fi
-
-			continue;
-		elif [ "${entry}" = "." ]; then
-			selection="${root}"
-		else
-			#
-			# Selection, in that case we check if the selection is a directory
-			# or a file by checking if the mpc ls on the selection returns
-			# more than one row. This is not the best but I can't find a better
-			# solution at the moment.
-			#
-
-			# Convert to absolute.
-			if [ -n "${root}" ]; then
-				entry="${root}/${entry}"
-			fi
-
-			list=$(mpc ls "${entry}")
-
-			if [ "${entry}" != "${list}" ]; then
-				root="${entry}"
-			else
-				selection="${entry}"
-			fi
-		fi
-	done
-
-	echo "${selection}"
-}
-
-#
-# Wrap a mpc command and execute dmt_fatal in case of errors.
-#
-dmt_mpc_run()
-{
-	path="/tmp/dmenutools-$(id -u).err"
-
-	if ! mpc "$@" > /dev/null 2> "${path}"; then
-		dmt_fatal "$(cat ${path})"
-		exit 1
-	fi
-}
-
-#
-# Echo all menu entries.
-#
-dmt_mpc_menu()
-{
-	echo "add..."
-	echo ""
-	echo "previous"
-	echo "next"
-	echo ""
-	echo "play"
-	echo "pause"
-	echo "stop"
-	echo "clear"
-	echo ""
-	echo "toggle consume"
-	echo "toggle repeat"
-	echo "shuffle queue"
-	echo ""
-	echo "update database"
-}
-
-cmd=$(dmt_mpc_menu | dmenu -p mpd -l 16)
-
-case ${cmd} in
-add*)
-	select="$(dmt_mpc_select)"
-
-	if [ -n "${select}" ]; then
-		dmt_mpc_run add "${select}"
-	fi
-	;;
-previous)
-	dmt_mpc_run prev
-	;;
-next)
-	dmt_mpc_run next
-	;;
-play)
-	dmt_mpc_run play
-	;;
-pause)
-	dmt_mpc_run pause
-	;;
-stop)
-	dmt_mpc_run stop
-	;;
-clear)
-	dmt_mpc_run clear
-	;;
-"toggle consume")
-	dmt_mpc_run consume
-	;;
-"toggle repeat")
-	dmt_mpc_run repeat
-	;;
-shuffle*)
-	dmt_mpc_run shuffle
-	;;
-update*)
-	dmt_mpc_run update
-	;;
-esac
--- a/bin/dmenu_power	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,172 +0,0 @@
-#!/bin/sh
-#
-# dmenu_power -- dmenu prompt for system shutdown
-#
-# Copyright (c) 2017-2018 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-TOOL="power"
-TOP=$(dirname "$(readlink -f "$0")")
-
-. ${TOP}/../libexec/dmenutools/dmenu.subr
-
-# Default user values.
-: "${power_hibernate_enable:=1}"
-: "${power_reboot_enable:=1}"
-: "${power_shutdown_enable:=1}"
-: "${power_suspend_enable:=1}"
-
-os=$(uname -s)
-
-# Check for systemctl on Linux.
-if command -v systemctl > /dev/null 2>&1; then
-    has_systemctl=1
-fi
-
-#
-# Create a list of commands.
-# -------------------------------------------------------------------
-#
-dmt_power_list()
-{
-    for i in hibernate reboot shutdown suspend; do
-        eval enabled="\${power_${i}_enable}"
-
-        if [ ${enabled} -eq 1 ]; then
-            list="${list}$i\n"
-        fi
-    done
-
-    echo -e $(echo ${list} | sed -e s'/\\n$//')
-}
-
-#
-# Do hibernation
-# -------------------------------------------------------------------
-#
-# Supports: Linux, OpenBSD
-#
-dmt_power_hibernate()
-{
-    if [ -n "${power_hibernate_cmd}" ]; then
-        ${power_hibernate_cmd}
-    elif [ "${has_systemctl}" -eq 1 ]; then
-        systemctl hibernate
-    else
-        case "${os}" in
-        Linux)
-            echo 'disk' | sudo -A tee -a /sys/power/state
-            ;;
-        OpenBSD)
-            ZZZ
-            ;;
-        *)
-            dmt_fatal "hibernation not supported on this system"
-            ;;
-        esac
-    fi
-}
-
-#
-# Do reboot
-# -------------------------------------------------------------------
-#
-# Supports: all
-#
-dmt_power_reboot()
-{
-    if [ -n "${power_reboot_cmd}" ]; then
-        ${power_reboot_cmd}
-    elif [ "${has_systemctl}" -eq 1 ]; then
-        systemctl reboot
-    else
-        sudo -A shutdown -r now
-    fi
-}
-
-#
-# Do shutdown
-# -------------------------------------------------------------------
-#
-# Supports: all
-#
-dmt_power_shutdown()
-{
-    if [ -n "${power_shutdown_cmd}" ]; then
-        ${power_shutdown_cmd}
-    elif [ "${has_systemctl}" -eq 1 ]; then
-        systemctl poweroff
-    else
-        case ${os} in
-        Linux)
-            sudo -A shutdown -h now
-            ;;
-        *)
-            sudo -A shutdown -p now
-            ;;
-        esac
-    fi
-}
-
-#
-# Do suspend
-# -------------------------------------------------------------------
-#
-# Supports: Linux, OpenBSD, FreeBSD (if has not been broken in a new release).
-#
-dmt_power_suspend()
-{
-    if [ -n "${power_suspend_cmd}" ]; then
-        ${power_suspend_cmd}
-    elif [ "${has_systemctl}" -eq 1 ]; then
-        systemctl suspend
-    else
-        case "${os}" in
-        Linux)
-            echo 'mem' | sudo -A tee -a /sys/power/state
-            ;;
-        FreeBSD)
-            # Good luck.
-            sudo -A acpiconf -s 3
-            ;;
-        OpenBSD)
-            zzz
-            ;;
-        *)
-            dmt_fatal "suspend not supported on this system"
-            ;;
-        esac
-    fi
-}
-
-cmd=$(dmt_power_list | dmenu -p "power")
-
-# TODO: change this with a substitution.
-case "${cmd}" in
-hibernate)
-    dmt_power_hibernate
-    ;;
-reboot)
-    dmt_power_reboot
-    ;;
-shutdown)
-    dmt_power_shutdown
-    ;;
-suspend)
-    dmt_power_suspend
-    ;;
-esac
-
-# vim: syntax=sh:
--- a/bin/dmenu_ssh	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-#!/bin/sh
-#
-# dmenu_ssh -- ssh prompt for dmenu
-#
-# Copyright (c) 2017-2018 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-TOOL="ssh"
-TOP=$(dirname "$(readlink -f "$0")")
-
-. ${TOP}/../libexec/dmenutools/dmenu.subr
-
-# Default user values.
-: "${ssh_lines:=0}"
-
-# Requirements.
-if [ -z "${term}" ]; then
-    dmt_fatal "no terminal set, see dmenutools.conf"
-fi
-if ! which ssh > /dev/null 2>&1; then
-    dmt_fatal "missing ssh"
-fi
-if [ ! -r "${HOME}/.ssh/config" ]; then
-    dmt_fatal "missing ${HOME}/.ssh/config" 1>&2;
-fi
-
-args="-p ssh"
-
-if [ "${ssh_lines}" -gt 0 ]; then
-    args="-l ${ssh_lines} ${args}"
-fi
-
-host=$(grep -Ei "Host\s" ${HOME}/.ssh/config | awk '{ print $2 '} | dmenu ${args})
-
-if [ -n "${host}" ]; then
-    ${term} -e ssh ${host}
-fi
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dmenu-background/CMakeLists.txt	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,21 @@
+#
+# CMakeLists.txt -- CMake build system for dmenutools
+#
+# Copyright (c) 2018 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+project(dmenu-background)
+add_executable(dmenu-background main.cpp)
+target_link_libraries(dmenu-background libdmenutools)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dmenu-background/main.cpp	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,132 @@
+/*
+ * dmenu-background.hpp -- apply a background
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <cstdlib>
+#include <iostream>
+#include <sstream>
+
+#include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
+
+#include <dmenu/dmenu.hpp>
+
+/*
+ * User configuration
+ * ------------------------------------------------------------------
+ *
+ * [background]
+ * path = "path to directory"
+ * command = "background command"
+ */
+
+namespace fs = boost::filesystem;
+
+namespace {
+
+class context {
+private:
+    fs::path path_;
+    std::string command_;
+
+    std::vector<std::string> list();
+
+public:
+    context();
+
+    boost::optional<std::string> select();
+
+    void apply(const std::string& file);
+};
+
+std::vector<std::string> context::list()
+{
+    std::vector<std::string> result;
+
+    if (path_.has_parent_path())
+        result.push_back("..");
+    for (fs::directory_iterator it(path_); it != fs::directory_iterator(); it++)
+        result.push_back(it->path().filename().string());
+
+    return result;
+}
+
+context::context()
+{
+    const auto section = dmenu::config("background");
+
+    path_ = section.get("path").get_value();
+    command_ = section.get("command").get_value();
+
+    if (path_.empty())
+        path_ = "/";
+    if (command_.empty())
+        command_ = "feh --bg-scale";
+}
+
+boost::optional<std::string> context::select()
+{
+    for (;;) {
+        const auto selection = dmenu::run({ "-l 16" }, list());
+
+        if (selection.empty())
+            return boost::none;
+
+        if (selection == "..") {
+            path_ = path_.parent_path();
+            continue;
+        }
+
+        const auto item = fs::path(path_) / selection;
+
+        switch (fs::status(item).type()) {
+        case fs::directory_file:
+            path_ = item;
+            break;
+        case fs::regular_file:
+            return item.string();
+        default:
+            throw std::runtime_error("invalid type file");
+        }
+    }
+
+    return boost::none;
+}
+
+void context::apply(const std::string& file)
+{
+    std::ostringstream oss;
+
+    oss << command_ << " " << file;
+
+    std::system(oss.str().c_str());
+}
+
+} // !namespace
+
+int main()
+{
+    try {
+        context ctx;
+
+        if (auto selection = ctx.select())
+            ctx.apply(*selection);
+    } catch (const std::exception& ex) {
+        std::cerr << "abort: " << ex.what() << std::endl;
+        return 1;
+    }
+}
--- a/doc/dmenu_bg.md	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-% DMENU_BG(1)
-% David Demelier <markand@malikania.fr>
-% November 2017
-
-# NAME
-
-dmenu_bg - select a background
-
-# SYNOPSIS
-
-**dmenu\_bg** [directory]
-
-# DESCRIPTION
-
-Open `dmenu_filesel` to select a file and apply it as background.
-
-# CONFIGURATION
-
-The following options are available in **dmenutools.conf**:
-
-**bg_directory**
-:	Select a directory, by default the script will check for `XDG_PICTURES_DIR`
-	environment variable and `$HOME` if none are set.
-
-**bg_cmd**
-:	Configure the tool to apply the wallpaper. If empty `dmenu_bg` will try
-    `feh`, `hsetroot` and `fbsetbg`.
-
-**bg_lines**
-:	How many lines to print for each files (Default: 16).
-
-# SEE ALSO
-
-`dmenutools`(1),
-`dmenu`(1).
--- a/doc/dmenu_filesel.md	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-% DMENU_FILESEL(1)
-% David Demelier <markand@malikania.fr>
-% November 2017
-
-# NAME
-
-dmenu_filesel - convenient dmenu file selector
-
-# SYNOPSIS
-
-**dmenu_filesel** [-l lines] [-t file|directory] [-p pattern] [directory]
-
-# DESCRIPTION
-
-Opens a dmenu tree to walk around the file system hierarchy.
-
-The following options are available:
-
-**-l lines**:
-:	Specify the amount of lines to show (Default: 16).
-
-**-t type**:
-:	Specify a type of file to select, if directory is specified, files will not
-	be displayed (Default: file).
-
-**-p pattern**:
-:	Set a file pattern to match. This option uses `egrep` to check matches.
-
-# SEE ALSO
-
-`dmenutools`(1),
-`dmenu`(1).
--- a/doc/dmenu_mpc.md	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,44 +0,0 @@
-% DMENU_MPD(1)
-% David Demelier <markand@malikania.fr>
-% November 2017
-
-# NAME
-
-dmenu_mpc - control music player daemon
-
-# SYNOPSIS
-
-**dmenu_mpc**
-
-# DESCRIPTION
-
-This dmenu tool let you control your music player daemon.
-
-You can control the song queue with basic functions such as, play, pause, stop,
-previous, next. You can also add songs using a dmenu tree selector.
-
-To select a whole directory, opens it and use '.' on it like this:
-
-    a
-    a/.
-    a/..
-    a/b
-    a/b/.   # select this line if you want to play the whole 'b' tree.
-    a/b/..
-    a/b/c
-
-# CONFIGURATION
-
-The following options are available in **dmenutools.conf**:
-
-**mpc_host**
-:	Hostname to connect, checked after `MPD_HOST` environment variable.
-
-**mpc_port**
-:	Port to use, checked after `MPD_PORT` environment variable.
-
-# SEE ALSO
-
-`dmenutools`(1),
-`dmenu`(1),
-`mpc`(1).
--- a/doc/dmenu_power.md	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-% DMENU_POWER(1)
-% David Demelier <markand@malikania.fr>
-% November 2017
-
-# NAME
-
-dmenu_power - turn off the system
-
-# SYNOPSIS
-
-**dmenu_power**
-
-# DESCRIPTION
-
-Show a menu to turn off the system and execute predefined actions.
-
-# CONFIGURATION
-
-By default, `dmenu_power` will propose to hibernate, shutdown, suspend and
-reboot. All these action can be disabled with the following options:
-
-**power_hibernate_enable**
-:	Set to 0 to disable this menu entry (Default: 1).
-
-**power_shutdown_enable**
-:	Set to 0 to disable this menu entry (Default: 1).
-
-**power_suspend_enable**
-:	Set to 0 to disable this menu entry (Default: 1).
-
-**power_reboot_enable**
-:	Set to 0 to disable this menu entry (Default: 1).
-
-All commands use defaults on appropriate system if available, it's possible to
-override them with the following options:
-
-**power_hibernate_cmd**
-:	Command to execute for hibernation.
-
-**power_shutdown_cmd**
-:	Command to execute to poweroff the system.
-
-**power_suspend_cmd**
-:	Command to execute to suspend the system.
-
-**power_reboot_cmd**
-:	Command to execute to reboot the system.
-
-# SECURITY
-
-This script uses `sudo -A` to execute commands, see `dmenutools(1)` to specify
-an optional sudo agent.
-
-# SEE ALSO
-
-`dmenutools`(1),
-`dmenu`(1).
--- a/doc/dmenu_ssh.md	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-% DMENU_SSH(1)
-% David Demelier <markand@malikania.fr>
-% November 2017
-
-# NAME
-
-dmenu_ssh - open a ssh session
-
-# SYNOPSIS
-
-**dmenu\_ssh**
-
-# DESCRIPTION
-
-This dmenu tool shows a list of configured ssh hosts in your
-**$HOME/.ssh/config** file and open a terminal to connect to it.
-
-You need to specify some hostnames in your **$HOME/.ssh/config** like this:
-
-    Host foo.org
-	Host example.org
-
-# CONFIGURATION
-
-The **dmenu_ssh** use the `term` option in **dmenutools.conf**.
-
-# SEE ALSO
-
-`dmenutools`(1),
-`dmenu`(1).
--- a/doc/dmenutools.md	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-% DMENUTOOLS(1)
-% David Demelier <markand@malikania.fr>
-% November 2017
-
-# NAME
-
-dmenutools - a set of dmenu utilities
-
-# DESCRIPTION
-
-A collection of utilities for `dmenu` to complete your window manager
-installation.
-
-# CONFIGURATION
-
-The dmenutools collection can be configured through a **dmenutools.conf** file
-read from the following paths:
-
-  - $XDG_CONFIG_HOME/dmenutools.conf
-  - $HOME/.config/dmenutools.conf
-
-The file is sourced from shell scripts so it can contain valid shell code. All
-modules may specify additional options, see their documentation.
-
-The following options are common to all:
-
-**term**
-:	A terminal to use, if empty several terminals are tested including xterm,
-	urxvt, gnome-terminal, st.
-
-# SUDO AGENT
-
-Some scripts may use `sudo -A` which can use an external tool to ask for user
-password. This means that you can use `dmenu` to prompt for a password to
-execute root commands.
-
-If you use sudo without a password, you have nothing else to do, otherwise, you
-can read the following section to enable a dmenu prompt.
-
-Create a `dmenu_sudo` script like this:
-
-	#!/bin/sh
-	# Use same color for foreground/background to hide your prompt.
-    dmenu -nb "black" -nf "black" <&- && echo
-
-Don't forget to make it executable:
-
-	chmod +x dmenu_sudo
-
-Then, fill the `SUDO_ASKPASS` variable to point to this executable:
-
-	export SUDO_ASKPASS=/usr/local/bin/dmenu_sudo
-
-# SEE ALSO
-
-`dmenu`(1).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenutools/CMakeLists.txt	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,33 @@
+#
+# CMakeLists.txt -- CMake build system for dmenutools
+#
+# Copyright (c) 2018 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+project(libdmenutools)
+
+set(
+    SOURCES
+    ${libdmenutools_SOURCE_DIR}/dmenu/dmenu.cpp
+    ${libdmenutools_SOURCE_DIR}/dmenu/dmenu.hpp
+    ${libdmenutools_SOURCE_DIR}/dmenu/ini.cpp
+    ${libdmenutools_SOURCE_DIR}/dmenu/ini.hpp
+    ${libdmenutools_SOURCE_DIR}/dmenu/xdg.hpp
+)
+
+add_library(libdmenutools STATIC ${SOURCES})
+set_target_properties(libdmenutools PROPERTIES PREFIX "")
+target_link_libraries(libdmenutools Boost::filesystem Boost::system)
+target_include_directories(libdmenutools PUBLIC ${libdmenutools_SOURCE_DIR})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenutools/dmenu/dmenu.cpp	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,85 @@
+/*
+ * dmenu.cpp -- dmenu utilities
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sstream>
+
+#include <boost/filesystem.hpp>
+#include <boost/process.hpp>
+
+#include "dmenu.hpp"
+#include "xdg.hpp"
+
+namespace fs = boost::filesystem;
+namespace proc = boost::process;
+
+namespace dmenu {
+
+namespace {
+
+std::string build_args(const std::vector<std::string>& args)
+{
+    std::ostringstream oss;
+
+    oss << "dmenu";
+
+    for (const auto& arg : args)
+        oss << " " << arg;
+
+    return oss.str();
+}
+
+std::string get_result(proc::ipstream& out, proc::child& child)
+{
+    std::string line;
+
+    if (child.running())
+        std::getline(out, line);
+        
+    child.wait();
+
+    return line;
+}
+
+} // !namespace
+
+std::string run(const std::vector<std::string>& args,
+                const std::vector<std::string>& lines)
+{
+    proc::opstream in;
+    proc::ipstream out;
+    proc::child child(build_args(args), proc::std_in < in, proc::std_out > out);
+
+    for (const auto& line : lines)
+        in << line << std::endl;
+
+    in.pipe().close();
+
+    return get_result(out, child);
+}
+
+ini::section config(const std::string& section)
+{
+    const auto path = xdg().config_home() + "/dmenutools.conf";
+
+    if (!fs::exists(path))
+        return ini::section(section);
+
+    return ini::read_file(path).get(section);
+}
+
+} // !dmenu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenutools/dmenu/dmenu.hpp	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,54 @@
+/*
+ * dmenu.hpp -- dmenu utilities
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef DMENUTOOLS_DMENU_HPP
+#define DMENUTOOLS_DMENU_HPP
+
+/**
+ * \file dmenu.hpp
+ * \brief dmenu utilities.
+ */
+
+#include <string>
+#include <vector>
+
+#include "ini.hpp"
+
+namespace dmenu {
+
+/**
+ * Invoke dmenu with the given arguments.
+ *
+ * \param args the dmenu arguments
+ * \param lines the stdin input for dmenu
+ * \return the selected entry
+ */
+std::string run(const std::vector<std::string>& args,
+                const std::vector<std::string>& lines);
+
+/**
+ * Get the configuration section.
+ *
+ * \param section the desired section (e.g. background)
+ * \return the section
+ */
+ini::section config(const std::string& section);
+
+} // !dmenu
+
+#endif // !DMENUTOOLS_DMENU_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenutools/dmenu/ini.cpp	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,423 @@
+/*
+ * ini.cpp -- extended .ini file parser
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <cctype>
+#include <cstring>
+#include <iostream>
+#include <iterator>
+#include <fstream>
+#include <sstream>
+#include <stdexcept>
+
+// for PathIsRelative.
+#if defined(_WIN32)
+#  include <Shlwapi.h>
+#endif
+
+#include "ini.hpp"
+
+namespace ini {
+
+namespace {
+
+using stream_iterator = std::istreambuf_iterator<char>;
+using token_iterator = std::vector<token>::const_iterator;
+
+inline bool is_absolute(const std::string& path) noexcept
+{
+#if defined(_WIN32)
+    return !PathIsRelative(path.c_str());
+#else
+    return path.size() > 0 && path[0] == '/';
+#endif
+}
+
+inline bool is_quote(char c) noexcept
+{
+    return c == '\'' || c == '"';
+}
+
+inline bool is_space(char c) noexcept
+{
+    // Custom version because std::isspace includes \n as space.
+    return c == ' ' || c == '\t';
+}
+
+inline bool is_list(char c) noexcept
+{
+    return c == '(' || c == ')' || c == ',';
+}
+
+inline bool is_reserved(char c) noexcept
+{
+    return is_list(c) || is_quote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '=';
+}
+
+void analyse_line(unsigned& line, unsigned& column, stream_iterator& it) noexcept
+{
+    assert(*it == '\n');
+
+    ++ line;
+    ++ it;
+    column = 0;
+}
+
+void analyse_comment(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
+{
+    assert(*it == '#');
+
+    while (it != end && *it != '\n') {
+        ++ column;
+        ++ it;
+    }
+}
+
+void analyse_spaces(unsigned& column, stream_iterator& it, stream_iterator end) noexcept
+{
+    assert(is_space(*it));
+
+    while (it != end && is_space(*it)) {
+        ++ column;
+        ++ it;
+    }
+}
+
+void analyse_list(tokens& list, unsigned line, unsigned& column, stream_iterator& it) noexcept
+{
+    assert(is_list(*it));
+
+    switch (*it++) {
+    case '(':
+        list.emplace_back(token::list_begin, line, column++);
+        break;
+    case ')':
+        list.emplace_back(token::list_end, line, column++);
+        break;
+    case ',':
+        list.emplace_back(token::comma, line, column++);
+        break;
+    default:
+        break;
+    }
+}
+
+void analyse_section(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+    assert(*it == '[');
+
+    std::string value;
+    unsigned save = column;
+
+    // Read section name.
+    for (++it; it != end && *it != ']';) {
+        if (*it == '\n')
+            throw exception(line, column, "section not terminated, missing ']'");
+        if (is_reserved(*it))
+            throw exception(line, column, "section name expected after '[', got '" + std::string(1, *it) + "'");
+
+        ++ column;
+        value += *it++;
+    }
+
+    if (it == end)
+        throw exception(line, column, "section name expected after '[', got <EOF>");
+    if (value.empty())
+        throw exception(line, column, "empty section name");
+
+    // Remove ']'.
+    ++ it;
+
+    list.emplace_back(token::section, line, save, std::move(value));
+}
+
+void analyse_assign(tokens& list, unsigned& line, unsigned& column, stream_iterator& it)
+{
+    assert(*it == '=');
+
+    list.push_back({ token::assign, line, column++ });
+    ++ it;
+}
+
+void analyse_quoted_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+    std::string value;
+    unsigned save = column;
+    char quote = *it++;
+
+    while (it != end && *it != quote) {
+        // TODO: escape sequence
+        ++ column;
+        value += *it++;
+    }
+
+    if (it == end)
+        throw exception(line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>");
+
+    // Remove quote.
+    ++ it;
+
+    list.push_back({ token::quoted_word, line, save, std::move(value) });
+}
+
+void analyse_word(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+    assert(!is_reserved(*it));
+
+    std::string value;
+    unsigned save = column;
+
+    while (it != end && !std::isspace(*it) && !is_reserved(*it)) {
+        ++ column;
+        value += *it++;
+    }
+
+    list.push_back({ token::word, line, save, std::move(value) });
+}
+
+void analyse_include(tokens& list, unsigned& line, unsigned& column, stream_iterator& it, stream_iterator end)
+{
+    assert(*it == '@');
+
+    std::string include;
+    unsigned save = column;
+
+    // Read include.
+    ++ it;
+    while (it != end && !is_space(*it)) {
+        ++ column;
+        include += *it++;
+    }
+
+    if (include == "include")
+        list.push_back({ token::include, line, save });
+    else if (include == "tryinclude")
+        list.push_back({ token::tryinclude, line, save });
+    else
+        throw exception(line, column, "expected include or tryinclude after '@' token");
+}
+
+void parse_option_value_simple(option& option, token_iterator& it)
+{
+    assert(it->get_type() == token::word || it->get_type() == token::quoted_word);
+
+    option.push_back((it++)->get_value());
+}
+
+void parse_option_value_list(option& option, token_iterator& it, token_iterator end)
+{
+    assert(it->get_type() == token::list_begin);
+
+    token_iterator save = it++;
+
+    while (it != end && it->get_type() != token::list_end) {
+        switch (it->get_type()) {
+        case token::comma:
+            // Previous must be a word.
+            if (it[-1].get_type() != token::word && it[-1].get_type() != token::quoted_word)
+                throw exception(it->get_line(), it->get_column(), "unexpected comma after '" + it[-1].get_value() + "'");
+
+            ++ it;
+            break;
+        case token::word:
+        case token::quoted_word:
+            option.push_back((it++)->get_value());
+            break;
+        default:
+            throw exception(it->get_line(), it->get_column(), "unexpected '" + it[-1].get_value() + "' in list construct");
+            break;
+        }
+    }
+
+    if (it == end)
+        throw exception(save->get_line(), save->get_column(), "unterminated list construct");
+
+    // Remove ).
+    ++ it;
+}
+
+void parse_option(section& sc, token_iterator& it, token_iterator end)
+{
+    option option(it->get_value());
+    token_iterator save(it);
+
+    // No '=' or something else?
+    if (++it == end)
+        throw exception(save->get_line(), save->get_column(), "expected '=' assignment, got <EOF>");
+    if (it->get_type() != token::assign)
+        throw exception(it->get_line(), it->get_column(), "expected '=' assignment, got " + it->get_value());
+
+    // Empty options are allowed so just test for words.
+    if (++it != end) {
+        if (it->get_type() == token::word || it->get_type() == token::quoted_word)
+            parse_option_value_simple(option, it);
+        else if (it->get_type() == token::list_begin)
+            parse_option_value_list(option, it, end);
+    }
+
+    sc.push_back(std::move(option));
+}
+
+void parse_include(document& doc, const std::string& path, token_iterator& it, token_iterator end, bool required)
+{
+    token_iterator save(it);
+
+    if (++it == end)
+        throw exception(save->get_line(), save->get_column(), "expected file name after '@include' statement, got <EOF>");
+    if (it->get_type() != token::word && it->get_type() != token::quoted_word)
+        throw exception(it->get_line(), it->get_column(), "expected file name after '@include' statement, got " + it->get_value());
+
+    std::string value = (it++)->get_value();
+    std::string file;
+
+    if (!is_absolute(value)) {
+#if defined(_WIN32)
+        file = path + "\\" + value;
+#else
+        file = path + "/" + value;
+#endif
+    } else
+        file = value;
+
+    try {
+        /*
+         * If required is set to true, we have @include, otherwise the non-fatal
+         * @tryinclude keyword.
+         */
+        for (const auto& sc : read_file(file))
+            doc.push_back(sc);
+    } catch (...) {
+        if (required)
+            throw;
+    }
+}
+
+void parse_section(document& doc, token_iterator& it, token_iterator end)
+{
+    section sc(it->get_value());
+
+    // Skip [section].
+    ++ it;
+
+    // Read until next section.
+    while (it != end && it->get_type() != token::section) {
+        if (it->get_type() != token::word)
+            throw exception(it->get_line(), it->get_column(), "unexpected token '" + it->get_value() + "' in section definition");
+
+        parse_option(sc, it, end);
+    }
+
+    doc.push_back(std::move(sc));
+}
+
+} // !namespace
+
+tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end)
+{
+    tokens list;
+    unsigned line = 1;
+    unsigned column = 0;
+
+    while (it != end) {
+        if (*it == '\n')
+            analyse_line(line, column, it);
+        else if (*it == '#')
+            analyse_comment(column, it, end);
+        else if (*it == '[')
+            analyse_section(list, line, column, it, end);
+        else if (*it == '=')
+            analyse_assign(list, line, column, it);
+        else if (is_space(*it))
+            analyse_spaces(column, it, end);
+        else if (*it == '@')
+            analyse_include(list, line, column, it, end);
+        else if (is_quote(*it))
+            analyse_quoted_word(list, line, column, it, end);
+        else if (is_list(*it))
+            analyse_list(list, line, column, it);
+        else
+            analyse_word(list, line, column, it, end);
+    }
+
+    return list;
+}
+
+tokens analyse(std::istream& stream)
+{
+    return analyse(std::istreambuf_iterator<char>(stream), {});
+}
+
+document parse(const tokens& tokens, const std::string& path)
+{
+    document doc;
+    token_iterator it = tokens.cbegin();
+    token_iterator end = tokens.cend();
+
+    while (it != end) {
+        switch (it->get_type()) {
+        case token::include:
+            parse_include(doc, path, it, end, true);
+            break;
+        case token::tryinclude:
+            parse_include(doc, path, it, end, false);
+            break;
+        case token::section:
+            parse_section(doc, it, end);
+            break;
+        default:
+            throw exception(it->get_line(), it->get_column(), "unexpected '" + it->get_value() + "' on root document");
+        }
+    }
+
+    return doc;
+}
+
+document read_file(const std::string& filename)
+{
+    // Get parent path.
+    auto parent = filename;
+    auto pos = parent.find_last_of("/\\");
+
+    if (pos != std::string::npos)
+        parent.erase(pos);
+    else
+        parent = ".";
+
+    std::ifstream input(filename);
+
+    if (!input)
+        throw exception(0, 0, std::strerror(errno));
+
+    return parse(analyse(input), parent);
+}
+
+document read_string(const std::string& buffer)
+{
+    std::istringstream iss(buffer);
+
+    return parse(analyse(iss));
+}
+
+void dump(const tokens& tokens)
+{
+    for (const token& token: tokens) {
+        // TODO: add better description
+        std::cout << token.get_line() << ":" << token.get_column() << ": " << token.get_value() << std::endl;
+    }
+}
+
+} // !ini
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenutools/dmenu/ini.hpp	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,675 @@
+/*
+ * ini.hpp -- extended .ini file parser
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef INI_HPP
+#define INI_HPP
+
+/**
+ * \file ini.hpp
+ * \brief Extended .ini file parser.
+ * \author David Demelier <markand@malikania.fr>
+ * \version 2.0.0
+ */
+
+/**
+ * \page Ini Ini
+ * \brief Extended .ini file parser.
+ *
+ * ## Export macros
+ *
+ * You must define `INI_DLL` globally and `INI_BUILDING_DLL` when compiling the
+ * library if you want a DLL, alternatively you can provide your own
+ * `INI_EXPORT` macro instead.
+ *
+ *   - \subpage ini-syntax
+ */
+
+/**
+ * \page ini-syntax Syntax
+ * \brief File syntax.
+ *
+ * The syntax is similar to most of `.ini` implementations as:
+ *
+ *   - a section is delimited by `[name]` can be redefined multiple times,
+ *   - an option **must** always be defined in a section,
+ *   - empty options must be surrounded by quotes,
+ *   - lists can not include trailing commas,
+ *   - include statements must always live at the beginning of files
+ *     (in no sections),
+ *   - comments start with # until the end of line,
+ *   - options with spaces **must** use quotes.
+ *
+ * # Basic file
+ *
+ * ````ini
+ * # This is a comment.
+ * [section]
+ * option1 = value1
+ * option2 = "value 2 with spaces"    # comment is also allowed here
+ * ````
+ *
+ * # Redefinition
+ *
+ * Sections can be redefined multiple times and are kept the order they are
+ * seen.
+ *
+ * ````ini
+ * [section]
+ * value = "1"
+ *
+ * [section]
+ * value = "2"
+ * ````
+ *
+ * The ini::document object will contains two ini::section.
+ *
+ * # Lists
+ *
+ * Lists are defined using `()` and commas, like values, they may have quotes.
+ *
+ * ````ini
+ * [section]
+ * names = ( "x1", "x2" )
+ *
+ * # This is also allowed.
+ * biglist = (
+ *   "abc",
+ *   "def"
+ * )
+ * ````
+ *
+ * # Include statement
+ *
+ * You can split a file into several pieces, if the include statement contains a
+ * relative path, the path will be relative to the current file being parsed.
+ *
+ * You **must** use the include statement before any section.
+ *
+ * If the file contains spaces, use quotes.
+ *
+ * ````ini
+ * # main.conf
+ * @include "foo.conf"
+ *
+ * # foo.conf
+ * [section]
+ * option1 = value1
+ * ````
+ */
+
+#include <algorithm>
+#include <cassert>
+#include <exception>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+/**
+ * \cond INI_HIDDEN_SYMBOLS
+ */
+
+#if !defined(INI_EXPORT)
+#   if defined(INI_DLL)
+#       if defined(_WIN32)
+#           if defined(INI_BUILDING_DLL)
+#               define INI_EXPORT __declspec(dllexport)
+#           else
+#               define INI_EXPORT __declspec(dllimport)
+#           endif
+#       else
+#           define INI_EXPORT
+#       endif
+#   else
+#       define INI_EXPORT
+#   endif
+#endif
+
+/**
+ * \endcond
+ */
+
+/**
+ * Namespace for ini related classes.
+ */
+namespace ini {
+
+class document;
+
+/**
+ * \brief exception in a file.
+ */
+class exception : public std::exception {
+private:
+    unsigned line_;
+    unsigned column_;
+    std::string message_;
+
+public:
+    /**
+     * Constructor.
+     *
+     * \param line the line
+     * \param column the column
+     * \param msg the message
+     */
+    inline exception(unsigned line, unsigned column, std::string msg) noexcept
+        : line_(line)
+        , column_(column)
+        , message_(std::move(msg))
+    {
+    }
+
+    /**
+     * Get the line number.
+     *
+     * \return the line
+     */
+    inline unsigned get_line() const noexcept
+    {
+        return line_;
+    }
+
+    /**
+     * Get the column number.
+     *
+     * \return the column
+     */
+    inline unsigned get_column() const noexcept
+    {
+        return column_;
+    }
+
+    /**
+     * Return the raw exception message (no line and column shown).
+     *
+     * \return the exception message
+     */
+    const char* what() const noexcept override
+    {
+        return message_.c_str();
+    }
+};
+
+/**
+ * \brief Describe a token read in the .ini source.
+ *
+ * This class can be used when you want to parse a .ini file yourself.
+ *
+ * \see analyse
+ */
+class token {
+public:
+    /**
+     * \brief token type.
+     */
+    enum type {
+        include,                //!< include statement
+        tryinclude,             //!< tryinclude statement
+        section,                //!< [section]
+        word,                   //!< word without quotes
+        quoted_word,            //!< word with quotes
+        assign,                 //!< = assignment
+        list_begin,             //!< begin of list (
+        list_end,               //!< end of list )
+        comma                   //!< list separation
+    };
+
+private:
+    type type_;
+    unsigned line_;
+    unsigned column_;
+    std::string value_;
+
+public:
+    /**
+     * Construct a token.
+     *
+     * \param type the type
+     * \param line the line
+     * \param column the column
+     * \param value the value
+     */
+    token(type type, unsigned line, unsigned column, std::string value = "") noexcept
+        : type_(type)
+        , line_(line)
+        , column_(column)
+    {
+        switch (type) {
+        case include:
+            value_ = "@include";
+            break;
+        case tryinclude:
+            value_ = "@tryinclude";
+            break;
+        case section:
+        case word:
+        case quoted_word:
+            value_ = value;
+            break;
+        case assign:
+            value_ = "=";
+            break;
+        case list_begin:
+            value_ = "(";
+            break;
+        case list_end:
+            value_ = ")";
+            break;
+        case comma:
+            value_ = ",";
+            break;
+        default:
+            break;
+        }
+    }
+
+    /**
+     * Get the type.
+     *
+     * \return the type
+     */
+    inline type get_type() const noexcept
+    {
+        return type_;
+    }
+
+    /**
+     * Get the line.
+     *
+     * \return the line
+     */
+    inline unsigned get_line() const noexcept
+    {
+        return line_;
+    }
+
+    /**
+     * Get the column.
+     *
+     * \return the column
+     */
+    inline unsigned get_column() const noexcept
+    {
+        return column_;
+    }
+
+    /**
+     * Get the value. For words, quoted words and section, the value is the
+     * content. Otherwise it's the characters parsed.
+     *
+     * \return the value
+     */
+    inline const std::string& get_value() const noexcept
+    {
+        return value_;
+    }
+};
+
+/**
+ * List of tokens in order they are analyzed.
+ */
+using tokens = std::vector<token>;
+
+/**
+ * \brief option definition.
+ */
+class option : public std::vector<std::string> {
+private:
+    std::string key_;
+
+public:
+    /**
+     * Construct an empty option.
+     *
+     * \pre key must not be empty
+     * \param key the key
+     */
+    inline option(std::string key) noexcept
+        : std::vector<std::string>()
+        , key_(std::move(key))
+    {
+        assert(!key_.empty());
+    }
+
+    /**
+     * Construct a single option.
+     *
+     * \pre key must not be empty
+     * \param key the key
+     * \param value the value
+     */
+    inline option(std::string key, std::string value) noexcept
+        : key_(std::move(key))
+    {
+        assert(!key_.empty());
+
+        push_back(std::move(value));
+    }
+
+    /**
+     * Construct a list option.
+     *
+     * \pre key must not be empty
+     * \param key the key
+     * \param values the values
+     */
+    inline option(std::string key, std::vector<std::string> values) noexcept
+        : std::vector<std::string>(std::move(values))
+        , key_(std::move(key))
+    {
+        assert(!key_.empty());
+    }
+
+    /**
+     * Get the option key.
+     *
+     * \return the key
+     */
+    inline const std::string& get_key() const noexcept
+    {
+        return key_;
+    }
+
+    /**
+     * Get the option value.
+     *
+     * \return the value
+     */
+    inline const std::string& get_value() const noexcept
+    {
+        static std::string dummy;
+
+        return empty() ? dummy : (*this)[0];
+    }
+};
+
+/**
+ * \brief Section that contains one or more options.
+ */
+class section : public std::vector<option> {
+private:
+    std::string key_;
+
+public:
+    /**
+     * Construct a section with its name.
+     *
+     * \pre key must not be empty
+     * \param key the key
+     */
+    inline section(std::string key) noexcept
+        : key_(std::move(key))
+    {
+        assert(!key_.empty());
+    }
+
+    /**
+     * Get the section key.
+     *
+     * \return the key
+     */
+    inline const std::string& get_key() const noexcept
+    {
+        return key_;
+    }
+
+    /**
+     * Check if the section contains a specific option.
+     *
+     * \param key the option key
+     * \return true if the option exists
+     */
+    inline bool contains(const std::string& key) const noexcept
+    {
+        return find(key) != end();
+    }
+
+    /**
+     * Find an option or return an empty one if not found.
+     *
+     * \param key the key
+     * \return the option or empty one if not found
+     */
+    inline option get(const std::string& key) const noexcept
+    {
+        auto it = find(key);
+
+        if (it == end())
+            return option(key);
+
+        return *it;
+    }
+
+    /**
+     * Find an option by key and return an iterator.
+     *
+     * \param key the key
+     * \return the iterator or end() if not found
+     */
+    inline iterator find(const std::string& key) noexcept
+    {
+        return std::find_if(begin(), end(), [&] (const auto& o) {
+            return o.get_key() == key;
+        });
+    }
+
+    /**
+     * Find an option by key and return an iterator.
+     *
+     * \param key the key
+     * \return the iterator or end() if not found
+     */
+    inline const_iterator find(const std::string& key) const noexcept
+    {
+        return std::find_if(cbegin(), cend(), [&] (const auto& o) {
+            return o.get_key() == key;
+        });
+    }
+
+    /**
+     * Access an option at the specified key.
+     *
+     * \param key the key
+     * \return the option
+     * \pre contains(key) must return true
+     */
+    inline option& operator[](const std::string& key)
+    {
+        assert(contains(key));
+
+        return *find(key);
+    }
+
+    /**
+     * Overloaded function.
+     *
+     * \param key the key
+     * \return the option
+     * \pre contains(key) must return true
+     */
+    inline const option& operator[](const std::string& key) const
+    {
+        assert(contains(key));
+
+        return *find(key);
+    }
+
+    /**
+     * Inherited operators.
+     */
+    using std::vector<option>::operator[];
+};
+
+/**
+ * \brief Ini document description.
+ * \see read_file
+ * \see read_string
+ */
+class document : public std::vector<section> {
+public:
+    /**
+     * Check if a document has a specific section.
+     *
+     * \param key the key
+     * \return true if the document contains the section
+     */
+    inline bool contains(const std::string& key) const noexcept
+    {
+        return find(key) != end();
+    }
+
+    /**
+     * Find a section or return an empty one if not found.
+     *
+     * \param key the key
+     * \return the section or empty one if not found
+     */
+    inline section get(const std::string& key) const noexcept
+    {
+        auto it = find(key);
+
+        if (it == end())
+            return section(key);
+
+        return *it;
+    }
+
+    /**
+     * Find a section by key and return an iterator.
+     *
+     * \param key the key
+     * \return the iterator or end() if not found
+     */
+    inline iterator find(const std::string& key) noexcept
+    {
+        return std::find_if(begin(), end(), [&] (const auto& o) {
+            return o.get_key() == key;
+        });
+    }
+
+    /**
+     * Find a section by key and return an iterator.
+     *
+     * \param key the key
+     * \return the iterator or end() if not found
+     */
+    inline const_iterator find(const std::string& key) const noexcept
+    {
+        return std::find_if(cbegin(), cend(), [&] (const auto& o) {
+            return o.get_key() == key;
+        });
+    }
+
+    /**
+     * Access a section at the specified key.
+     *
+     * \param key the key
+     * \return the section
+     * \pre contains(key) must return true
+     */
+    inline section& operator[](const std::string& key)
+    {
+        assert(contains(key));
+
+        return *find(key);
+    }
+
+    /**
+     * Overloaded function.
+     *
+     * \param key the key
+     * \return the section
+     * \pre contains(key) must return true
+     */
+    inline const section& operator[](const std::string& key) const
+    {
+        assert(contains(key));
+
+        return *find(key);
+    }
+
+    /**
+     * Inherited operators.
+     */
+    using std::vector<section>::operator[];
+};
+
+/**
+ * Analyse a stream and detect potential syntax errors. This does not parse the
+ * file like including other files in include statement.
+ *
+ * It does only analysis, for example if an option is defined under no section,
+ * this does not trigger an exception while it's invalid.
+ *
+ * \param it the iterator
+ * \param end where to stop
+ * \return the list of tokens
+ * \throws exception on errors
+ */
+INI_EXPORT tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end);
+
+/**
+ * Overloaded function for stream.
+ *
+ * \param stream the stream
+ * \return the list of tokens
+ * \throws exception on errors
+ */
+INI_EXPORT tokens analyse(std::istream& stream);
+
+/**
+ * Parse the produced tokens.
+ *
+ * \param tokens the tokens
+ * \param path the parent path
+ * \return the document
+ * \throw exception on errors
+ */
+INI_EXPORT document parse(const tokens& tokens, const std::string& path = ".");
+
+/**
+ * Parse a file.
+ *
+ * \param filename the file name
+ * \return the document
+ * \throw exception on errors
+ */
+INI_EXPORT document read_file(const std::string& filename);
+
+/**
+ * Parse a string.
+ *
+ * If the string contains include statements, they are relative to the current
+ * working directory.
+ *
+ * \param buffer the buffer
+ * \return the document
+ * \throw exception on exceptions
+ */
+INI_EXPORT document read_string(const std::string& buffer);
+
+/**
+ * Show all tokens and their description.
+ *
+ * \param tokens the tokens
+ */
+INI_EXPORT void dump(const tokens& tokens);
+
+} // !ini
+
+#endif // !INI_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenutools/dmenu/xdg.hpp	Thu Apr 26 13:23:44 2018 +0200
@@ -0,0 +1,194 @@
+/*
+ * xdg.hpp -- XDG directory specifications
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef DMENUTOOLS_XDG_HPP
+#define DMENUTOOLS_XDG_HPP
+
+/**
+ * \file xdg.hpp
+ * \brief XDG directory specifications.
+ * \author David Demelier <markand@malikana.fr>
+ */
+
+#include <cstdlib>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace dmenu {
+
+/**
+ * \brief XDG directory specifications.
+ *
+ * Read and get XDG directories.
+ *
+ * This file should compiles on Windows to facilitate portability but its
+ * functions must not be used.
+ */
+class xdg {
+private:
+    std::string config_home_;
+    std::string data_home_;
+    std::string cache_home_;
+    std::string runtime_dir_;
+    std::vector<std::string> config_dirs_;
+    std::vector<std::string> data_dirs_;
+
+    inline bool is_absolute(const std::string& path) const noexcept
+    {
+        return path.length() > 0 && path[0] == '/';
+    }
+
+    std::vector<std::string> split(const std::string& arg) const
+    {
+        std::stringstream iss(arg);
+        std::string item;
+        std::vector<std::string> elems;
+
+        while (std::getline(iss, item, ':')) {
+            if (is_absolute(item))
+                elems.push_back(item);
+        }
+
+        return elems;
+    }
+
+    std::string env_or_home(const std::string& var, const std::string& repl) const
+    {
+        auto value = std::getenv(var.c_str());
+
+        if (value == nullptr || !is_absolute(value)) {
+            auto home = std::getenv("HOME");
+
+            if (home == nullptr)
+                throw std::runtime_error("could not get home directory");
+
+            return std::string(home) + "/" + repl;
+        }
+
+        return value;
+    }
+
+    std::vector<std::string> list_or_defaults(const std::string& var,
+                                              const std::vector<std::string>& list) const
+    {
+        auto value = std::getenv(var.c_str());
+
+        if (!value)
+            return list;
+
+        // No valid item at all? Use defaults.
+        auto result = split(value);
+
+        return (result.size() == 0) ? list : result;
+    }
+
+public:
+    /**
+     * Open an xdg instance and load directories.
+     *
+     * \throw std::runtime_error on failures
+     */
+    xdg()
+    {
+        config_home_    = env_or_home("XDG_CONFIG_HOME", ".config");
+        data_home_      = env_or_home("XDG_DATA_HOME", ".local/share");
+        cache_home_     = env_or_home("XDG_CACHE_HOME", ".cache");
+
+        config_dirs_    = list_or_defaults("XDG_CONFIG_DIRS", { "/etc/xdg" });
+        data_dirs_      = list_or_defaults("XDG_DATA_DIRS", { "/usr/local/share", "/usr/share" });
+
+        /*
+         * Runtime directory is a special case and does not have a replacement,
+         * the application should manage this by itself.
+         */
+        auto runtime = std::getenv("XDG_RUNTIME_DIR");
+
+        if (runtime && is_absolute(runtime))
+            runtime_dir_ = runtime;
+    }
+
+    /**
+     * Get the config directory. ${XDG_CONFIG_HOME} or ${HOME}/.config
+     *
+     * \return the config directory
+     */
+    inline const std::string& config_home() const noexcept
+    {
+        return config_home_;
+    }
+
+    /**
+     * Get the data directory. ${XDG_DATA_HOME} or ${HOME}/.local/share
+     *
+     * \return the data directory
+     */
+    inline const std::string& data_home() const noexcept
+    {
+        return data_home_;
+    }
+
+    /**
+     * Get the cache directory. ${XDG_CACHE_HOME} or ${HOME}/.cache
+     *
+     * \return the cache directory
+     */
+    inline const std::string& cache_home() const noexcept
+    {
+        return cache_home_;
+    }
+
+    /**
+     * Get the runtime directory.
+     *
+     * There is no replacement for XDG_RUNTIME_DIR, if it is not set, an empty
+     * value is returned and the user is responsible of using something else.
+     *
+     * \return the runtime directory
+     */
+    inline const std::string& runtime_dir() const noexcept
+    {
+        return runtime_dir_;
+    }
+
+    /**
+     * Get the standard config directories. ${XDG_CONFIG_DIRS} or { "/etc/xdg" }
+     *
+     * \return the list of config directories
+     */
+    inline const std::vector<std::string>& config_dirs() const noexcept
+    {
+        return config_dirs_;
+    }
+
+    /**
+     * Get the data directories. ${XDG_DATA_DIRS} or { "/usr/local/share",
+     * "/usr/share" }
+     *
+     * \return the list of data directories
+     */
+    inline const std::vector<std::string>& data_dirs() const noexcept
+    {
+        return data_dirs_;
+    }
+};
+
+} // !dmenu
+
+#endif // !XDG_HPP
--- a/libexec/dmenutools/dmenu.subr	Tue Jan 02 13:27:42 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-#!/bin/sh
-#
-# dmenu.subr -- common operations for dmenutools
-#
-# Copyright (c) 2017-2018 David Demelier <markand@malikania.fr>
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-#
-
-# Directory for configuration.
-if [ -z "${XDG_CONFIG_HOME}" ]; then
-    XDG_CONFIG_HOME=${HOME}/.config
-fi
-
-# Open user optional configuration.
-if [ -r ${XDG_CONFIG_HOME}/dmenutools.conf ]; then
-    . ${XDG_CONFIG_HOME}/dmenutools.conf
-fi
-
-# Find a terminal.
-if [ -z "${term}" ]; then
-    for t in urxvt st terminator terminator xterm; do
-        if command -v ${t} > /dev/null 2>&1; then
-            term=${t}
-            break
-        fi
-    done
-fi
-
-#
-# dmt_fatal
-# -------------------------------------------------------------------
-#
-# Log a fatal error through fatal_cmd helper.
-# 
-dmt_fatal()
-{
-    ${fatal_cmd} "$1"
-    exit 1
-}
-
-#
-# dmt_run
-# -------------------------------------------------------------------
-#
-# Run dmenu with parameters specified from both configuration and arguments.
-#
-dmt_run()
-{
-    dmenu $*
-}
-
-#
-# dmt_boolean
-# -------------------------------------------------------------------
-# 
-# Check if a variable is a boolean value.
-#
-dmt_boolean()
-{
-    v=$(echo $1 | tr '[:upper:]' '[:lower:]')
-
-    if [ $v = "1" ] || [ $v = "yes" ] || [ $v = "on" ] || [ $v = "true" ]; then
-        return 0
-    fi
-
-    return 1
-}
-
-# Determine a fatal helper.
-if [ -z "${fatal_cmd}" ]; then
-    if command -v notify-send > /dev/null 2>&1; then
-        fatal_cmd="notify-send"
-    elif command -v xmessage > /dev/null 2>&1; then
-        fatal_cmd="xmessage"
-    else
-        fatal_cmd="echo"
-    fi
-fi
-
-if ! command -v dmenu > /dev/null 2>&1; then
-    dmt_fatal "dmenu is missing"
-fi
-
-# vim: syntax=sh:

mercurial