changeset 36:b970ba24bdef

misc: rename dmenutools to dmenu
author David Demelier <markand@malikania.fr>
date Tue, 01 May 2018 12:32:16 +0200
parents 72274722586f
children 76d774822dac
files CMakeLists.txt cmake/function/DefineExecutable.cmake libdmenu/CMakeLists.txt libdmenu/dmenu/dmenu.cpp libdmenu/dmenu/dmenu.hpp libdmenu/dmenu/ini.cpp libdmenu/dmenu/ini.hpp libdmenu/dmenu/join.hpp libdmenu/dmenu/sysconfig.hpp.in libdmenu/dmenu/xdg.hpp libdmenutools/CMakeLists.txt libdmenutools/dmenu/dmenu.cpp libdmenutools/dmenu/dmenu.hpp libdmenutools/dmenu/ini.cpp libdmenutools/dmenu/ini.hpp libdmenutools/dmenu/join.hpp libdmenutools/dmenu/sysconfig.hpp.in libdmenutools/dmenu/xdg.hpp
diffstat 18 files changed, 1625 insertions(+), 1625 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Tue May 01 12:16:12 2018 +0200
+++ b/CMakeLists.txt	Tue May 01 12:32:16 2018 +0200
@@ -27,7 +27,7 @@
 find_package(Boost REQUIRED COMPONENTS filesystem system)
 find_package(PkgConfig REQUIRED)
 
-add_subdirectory(libdmenutools)
+add_subdirectory(libdmenu)
 add_subdirectory(dmenu-background)
 add_subdirectory(dmenu-mpc)
 add_subdirectory(dmenu-ssh)
--- a/cmake/function/DefineExecutable.cmake	Tue May 01 12:16:12 2018 +0200
+++ b/cmake/function/DefineExecutable.cmake	Tue May 01 12:32:16 2018 +0200
@@ -60,7 +60,7 @@
         add_executable(dmenu-${EXE_NAME} ${EXE_SOURCES})
         target_include_directories(dmenu-${EXE_NAME} PRIVATE ${EXE_INCLUDES})
         target_compile_definitions(dmenu-${EXE_NAME} PRIVATE ${EXE_FLAGS})
-        target_link_libraries(dmenu-${EXE_NAME} libdmenutools ${EXE_LIBRARIES})
+        target_link_libraries(dmenu-${EXE_NAME} libdmenu ${EXE_LIBRARIES})
 
         install(
             TARGETS dmenu-${EXE_NAME}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenu/CMakeLists.txt	Tue May 01 12:32:16 2018 +0200
@@ -0,0 +1,64 @@
+#
+# 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(libdmenu)
+
+pkg_check_modules(libnotify IMPORTED_TARGET libnotify)
+
+option(WITH_NOTIFY "Enable libnotify support" On)
+
+if (NOT WITH_NOTIFY)
+    set(WITH_NOTIFY_MSG "No (disabled by user)" CACHE INTERNAL "" FORCE)
+elseif (NOT libnotify_FOUND)
+    set(WITH_NOTIFY_MSG "No (libnotify not found)" CACHE INTERNAL "" FORCE)
+else ()
+    set(WITH_NOTIFY_MSG "Yes" CACHE INTERNAL "" FORCE)
+    set(HAVE_NOTIFY On)
+endif ()
+
+set(
+    SOURCES
+    ${libdmenu_SOURCE_DIR}/dmenu/dmenu.cpp
+    ${libdmenu_SOURCE_DIR}/dmenu/dmenu.hpp
+    ${libdmenu_SOURCE_DIR}/dmenu/ini.cpp
+    ${libdmenu_SOURCE_DIR}/dmenu/ini.hpp
+    ${libdmenu_SOURCE_DIR}/dmenu/xdg.hpp
+)
+
+configure_file(
+    ${libdmenu_SOURCE_DIR}/dmenu/sysconfig.hpp.in
+    ${libdmenu_BINARY_DIR}/dmenu/sysconfig.hpp
+)
+
+add_library(libdmenu STATIC ${SOURCES})
+set_target_properties(libdmenu PROPERTIES PREFIX "")
+
+target_link_libraries(
+    libdmenu
+    Boost::filesystem
+    Boost::system
+    $<$<BOOL:${HAVE_NOTIFY}>:PkgConfig::libnotify>
+)
+
+target_include_directories(
+    libdmenu
+    PUBLIC
+        ${libdmenu_SOURCE_DIR}
+    PRIVATE
+        ${libdmenu_BINARY_DIR}/dmenu
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenu/dmenu/dmenu.cpp	Tue May 01 12:32:16 2018 +0200
@@ -0,0 +1,109 @@
+/*
+ * dmenu.cpp -- dmenu utilities
+ *
+ * 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.
+ */
+
+#include <iostream>
+#include <sstream>
+
+#include <boost/filesystem.hpp>
+#include <boost/process.hpp>
+
+#include "dmenu.hpp"
+#include "sysconfig.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);
+}
+
+void error(const std::string& summary, const std::string& message)
+{
+#if defined(HAVE_NOTIFY)
+    static bool is_initialized;
+
+    if (!is_initialized) {
+        notify_init("dmenu");
+        is_initialized = true;
+    }
+
+    NotifyNotification* n = notify_notification_new(summary.c_str(), message.c_str(), nullptr);
+    GError* error = nullptr;
+
+    if (!notify_notification_show(n, &error))
+        std::cerr << error->message << std::endl;
+
+    g_free(error);
+#else
+    std::cerr << summary << ": " << message << std::endl;
+#endif
+}
+
+} // !dmenu
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenu/dmenu/dmenu.hpp	Tue May 01 12:32:16 2018 +0200
@@ -0,0 +1,64 @@
+/*
+ * dmenu.hpp -- dmenu utilities
+ *
+ * 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.
+ */
+
+#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);
+
+/**
+ * Show a notification error.
+ *
+ * If notification support is disabled, error is printed to std::cerr.
+ *
+ * \param summary the summary
+ * \param message the message content
+ */
+void error(const std::string& summary, const std::string& message);
+
+} // !dmenu
+
+#endif // !DMENUTOOLS_DMENU_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenu/dmenu/ini.cpp	Tue May 01 12:32:16 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/libdmenu/dmenu/ini.hpp	Tue May 01 12:32:16 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
+ *