├── test ├── int_test │ ├── sys.ini │ ├── config.ini │ └── xml │ │ ├── section1.xml │ │ ├── sectionobj.xml │ │ └── section2.xml ├── config_lock.ini ├── dummy.ini ├── expect_line.hpp ├── section_test.cpp ├── option_wrapper_test.cpp ├── option_base_test.cpp ├── meson.build ├── config_manager_test.cpp ├── log_test.cpp ├── number_locale_test.cpp ├── duration_test.cpp ├── option_test.cpp ├── file_test.cpp └── xml_test.cpp ├── meson_options.txt ├── src ├── section-impl.hpp ├── option-impl.hpp ├── option.cpp ├── section.cpp ├── config-manager.cpp ├── log.cpp ├── compound-option.cpp ├── duration.cpp ├── xml.cpp └── file.cpp ├── include ├── meson.build └── wayfire │ ├── config │ ├── option-types.hpp │ ├── config-manager.hpp │ ├── section.hpp │ ├── xml.hpp │ ├── file.hpp │ ├── option-wrapper.hpp │ ├── option.hpp │ ├── compound-option.hpp │ └── types.hpp │ ├── util │ ├── stringify.hpp │ ├── log.hpp │ └── duration.hpp │ └── nonstd │ └── safe-list.hpp ├── .github └── workflows │ └── ci.yaml ├── LICENSE └── meson.build /test/int_test/sys.ini: -------------------------------------------------------------------------------- 1 | [section2] 2 | option5 = Option5Sys 3 | -------------------------------------------------------------------------------- /test/config_lock.ini: -------------------------------------------------------------------------------- 1 | 2 | [section1] 3 | 4 | option1 = 12 5 | 6 | [section2] 7 | 8 | option2 = opt2 9 | -------------------------------------------------------------------------------- /test/dummy.ini: -------------------------------------------------------------------------------- 1 | [section1] 2 | option1 = 4 3 | option2 = 45 \# 46 \\ 4 | 5 | [section2] 6 | bey_k1 = 1.200000 7 | hey_k1 = 1 8 | option1 = 4.250000 9 | 10 | -------------------------------------------------------------------------------- /test/int_test/config.ini: -------------------------------------------------------------------------------- 1 | 2 | [section1] 3 | 4 | option1 = 12 5 | 6 | [section2] 7 | 8 | option2 = opt2 9 | option3 = DoesNotExistInXML \# \\ 10 | 11 | [sectionobj:objtest] 12 | 13 | option6 = 11 14 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('tests', type: 'feature', value: 'auto', description: 'Enable unit tests') 2 | option('locale_test', type : 'boolean', value : false, description: 'Test number to string conversions with de_DE locale (must be installed)') 3 | -------------------------------------------------------------------------------- /test/int_test/xml/section1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | General 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/int_test/xml/sectionobj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | General 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/section-impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct wf::config::section_t::impl 8 | { 9 | public: 10 | std::map> options; 11 | std::string name; 12 | 13 | // Associated XML node 14 | xmlNode *xml = NULL; 15 | }; 16 | -------------------------------------------------------------------------------- /test/int_test/xml/section2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | General 5 | 8 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/expect_line.hpp: -------------------------------------------------------------------------------- 1 | auto EXPECT_LINE = [] (std::istream& log, std::string expect) 2 | { 3 | auto tolower = [] (std::string s) 4 | { 5 | for (auto& c : s) 6 | { 7 | c = std::tolower(c); 8 | } 9 | 10 | return s; 11 | }; 12 | 13 | bool found = false; 14 | 15 | std::string line; 16 | while (!found && std::getline(log, line)) 17 | { 18 | /* Case-insensitive matching */ 19 | line = tolower(line); 20 | expect = tolower(expect); 21 | found |= (line.find(expect) != std::string::npos); 22 | } 23 | 24 | CHECK(found); 25 | }; 26 | -------------------------------------------------------------------------------- /include/meson.build: -------------------------------------------------------------------------------- 1 | headers_config = [ 2 | 'wayfire/config/xml.hpp', 3 | 'wayfire/config/config-manager.hpp', 4 | 'wayfire/config/section.hpp', 5 | 'wayfire/config/option-types.hpp', 6 | 'wayfire/config/types.hpp', 7 | 'wayfire/config/file.hpp', 8 | 'wayfire/config/option.hpp', 9 | 'wayfire/config/option-wrapper.hpp', 10 | 'wayfire/config/compound-option.hpp', 11 | ] 12 | 13 | headers_util = [ 14 | 'wayfire/util/log.hpp', 15 | 'wayfire/util/stringify.hpp', 16 | 'wayfire/util/duration.hpp' 17 | ] 18 | 19 | headers_nonstd = [ 20 | 'wayfire/nonstd/safe-list.hpp' 21 | ] 22 | 23 | install_headers(headers_config, subdir: 'wayfire/config') 24 | install_headers(headers_util, subdir: 'wayfire/util') 25 | install_headers(headers_nonstd, subdir: 'wayfire/nonstd') 26 | -------------------------------------------------------------------------------- /include/wayfire/config/option-types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace wf 7 | { 8 | namespace option_type 9 | { 10 | /** 11 | * To create an option of a given type, from_string must be specialized for 12 | * parsing the type. 13 | * 14 | * @param string The string representation of the value. 15 | * @return The parsed value, if the string was valid. 16 | */ 17 | template 18 | std::optional from_string( 19 | const std::string& string); 20 | 21 | /** 22 | * To create an option of a given type, to_string must be specialized for 23 | * converting the type to string. 24 | * @return The string representation of a value. 25 | * It is expected that from_string(to_string(value)) == value. 26 | */ 27 | template 28 | std::string to_string(const Type& value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | test_code_style: 6 | name: "Check code style with uncrustify" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: sudo apt-get update 10 | - run: sudo apt-get install -y git cmake gcc make 11 | - uses: actions/checkout@v1 12 | - run: git clone http://github.com/ammen99/uncrustify 13 | - run: cd uncrustify && mkdir build && cd build && cmake ../ && make && cd ../../ 14 | - run: curl https://raw.githubusercontent.com/WayfireWM/wayfire/master/uncrustify.ini > uncrustify.ini 15 | - run: git ls-files | grep "hpp$\|cpp$" | xargs ./uncrustify/build/uncrustify -c uncrustify.ini --check 16 | run_tests: 17 | name: "Check that tests do not break" 18 | runs-on: ubuntu-latest 19 | steps: 20 | - run: sudo apt-get update 21 | - run: sudo apt-get install -y cmake git gcc meson doctest-dev libevdev-dev libxml2-dev libglm-dev 22 | - uses: actions/checkout@v1 23 | - run: meson build 24 | - run: ninja -C build test 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2019 Ilia Bozhinov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /include/wayfire/util/stringify.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace wf 7 | { 8 | namespace log 9 | { 10 | /** 11 | * Convert the given parameter to a string which can be logged. 12 | * This function can be specialized for custom types. 13 | */ 14 | template 15 | std::string to_string(T arg) 16 | { 17 | std::ostringstream out; 18 | out << arg; 19 | return out.str(); 20 | } 21 | 22 | /** Specialization for boolean arguments - print true or false. */ 23 | template<> 24 | std::string to_string(bool arg); 25 | 26 | /* Specialization for pointers - print the address */ 27 | template 28 | std::string to_string(T *arg) 29 | { 30 | if (!arg) 31 | { 32 | return "(null)"; 33 | } 34 | 35 | return to_string(arg); 36 | } 37 | 38 | namespace detail 39 | { 40 | /** 41 | * Convert each argument to a string and then concatenate them. 42 | */ 43 | template 44 | std::string format_concat(First arg) 45 | { 46 | return wf::log::to_string(arg); 47 | } 48 | 49 | template 50 | std::string format_concat(First first, Args... args) 51 | { 52 | return format_concat(first) + format_concat(args...); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/option-impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace wf 10 | { 11 | namespace config 12 | { 13 | /** 14 | * Update the value of a compound option option by reading options from the section. 15 | * The format is as described in the compound option constructor docstring. 16 | * 17 | * Note: options which have been created from XML are ignored, and only 18 | * options which have been created from parsing a string/file with wf-config 19 | * are taken into account. 20 | */ 21 | void update_compound_from_section(compound_option_t& option, 22 | const std::shared_ptr& section); 23 | } 24 | } 25 | 26 | struct wf::config::option_base_t::impl 27 | { 28 | std::string name; 29 | wf::safe_list_t updated_handlers; 30 | 31 | // Number of times the option has been locked 32 | int32_t lock_count = 0; 33 | 34 | // Associated XML node 35 | xmlNode *xml = nullptr; 36 | 37 | // Is option in config file? 38 | bool option_in_config_file = false; 39 | 40 | // Is option part of a successfully parsed compound option? 41 | bool is_part_compound = false; 42 | // Does this option match a compound option in part at least? 43 | bool could_be_compound = false; 44 | }; 45 | -------------------------------------------------------------------------------- /src/option.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "option-impl.hpp" 6 | #include "wayfire/util/log.hpp" 7 | 8 | const std::string& wf::config::option_base_t::get_name() const 9 | { 10 | return this->priv->name; 11 | } 12 | 13 | void wf::config::option_base_t::add_updated_handler( 14 | updated_callback_t *callback) 15 | { 16 | this->priv->updated_handlers.push_back(callback); 17 | } 18 | 19 | void wf::config::option_base_t::rem_updated_handler( 20 | updated_callback_t *callback) 21 | { 22 | priv->updated_handlers.remove_all(callback); 23 | } 24 | 25 | wf::config::option_base_t::option_base_t(const std::string& name) 26 | { 27 | this->priv = std::make_unique(); 28 | this->priv->name = name; 29 | } 30 | 31 | wf::config::option_base_t::~option_base_t() = default; 32 | 33 | void wf::config::option_base_t::notify_updated() const 34 | { 35 | priv->updated_handlers.for_each([] (updated_callback_t *call) 36 | { 37 | (*call)(); 38 | }); 39 | } 40 | 41 | void wf::config::option_base_t::set_locked(bool locked) 42 | { 43 | this->priv->lock_count += (locked ? 1 : -1); 44 | if (priv->lock_count < 0) 45 | { 46 | LOGE("Lock counter for option ", this->get_name(), " dropped below zero!"); 47 | } 48 | } 49 | 50 | bool wf::config::option_base_t::is_locked() const 51 | { 52 | return this->priv->lock_count > 0; 53 | } 54 | 55 | void wf::config::option_base_t::init_clone(option_base_t& other) const 56 | { 57 | other.priv->xml = this->priv->xml; 58 | other.priv->name = this->priv->name; 59 | } 60 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'wf-config', 3 | 'cpp', 4 | version: '0.11.0', 5 | license: 'MIT', 6 | meson_version: '>=0.47.0', 7 | default_options: [ 8 | 'cpp_std=c++17', 9 | 'warning_level=2', 10 | 'werror=false', 11 | ], 12 | ) 13 | 14 | add_project_arguments(['-Wno-deprecated-declarations'], language: ['cpp']) 15 | 16 | glm = dependency('glm', required: false) 17 | if not glm.found() and not meson.get_compiler('cpp').check_header('glm/glm.hpp') 18 | error('GLM not found, and directly using the header \'glm/glm.hpp\' is not possible.') 19 | endif 20 | 21 | evdev = dependency('libevdev') 22 | libxml2 = dependency('libxml-2.0') 23 | 24 | sources = [ 25 | 'src/types.cpp', 26 | 'src/option.cpp', 27 | 'src/section.cpp', 28 | 'src/log.cpp', 29 | 'src/xml.cpp', 30 | 'src/config-manager.cpp', 31 | 'src/file.cpp', 32 | 'src/duration.cpp', 33 | 'src/compound-option.cpp', 34 | ] 35 | 36 | wfconfig_inc = include_directories('include') 37 | 38 | lib_wfconfig = library('wf-config', 39 | sources, 40 | dependencies: [evdev, glm, libxml2], 41 | include_directories: wfconfig_inc, 42 | install: true, 43 | version: meson.project_version(), 44 | soversion: '1') 45 | 46 | pkgconfig = import('pkgconfig') 47 | pkgconfig.generate( 48 | libraries: lib_wfconfig, 49 | version: meson.project_version(), 50 | filebase: meson.project_name(), 51 | name: meson.project_name(), 52 | description: 'Dynamic file-based configuration library for Wayfire') 53 | 54 | install_headers([], subdir: 'wayfire/config') 55 | 56 | wfconfig = declare_dependency(link_with: lib_wfconfig, 57 | include_directories: wfconfig_inc, 58 | dependencies: glm) 59 | 60 | # Install headers 61 | subdir('include') 62 | 63 | # Unit tests 64 | doctest = dependency('doctest', required: get_option('tests')) 65 | 66 | if doctest.found() 67 | subdir('test') 68 | endif 69 | -------------------------------------------------------------------------------- /test/section_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | 4 | #include 5 | #include 6 | #include "../src/section-impl.hpp" 7 | 8 | TEST_CASE("wf::config::section_t") 9 | { 10 | using namespace wf; 11 | using namespace wf::config; 12 | 13 | section_t section{"Test_Section-12.34"}; 14 | CHECK(section.get_name() == "Test_Section-12.34"); 15 | CHECK(section.get_registered_options().empty()); 16 | CHECK(section.get_option_or("non_existing") == nullptr); 17 | 18 | auto intopt = std::make_shared>("IntOption", 123); 19 | section.register_new_option(intopt); 20 | 21 | CHECK(section.get_option("IntOption") == intopt); 22 | CHECK(section.get_option_or("IntOption") == intopt); 23 | CHECK(section.get_option_or("DoubleOption") == nullptr); 24 | 25 | auto reg_opts = section.get_registered_options(); 26 | REQUIRE(reg_opts.size() == 1); 27 | CHECK(reg_opts.back() == intopt); 28 | 29 | auto intopt2 = std::make_shared>("IntOption", 125); 30 | section.register_new_option(intopt2); // overwrite 31 | CHECK(section.get_option_or("IntOption") == intopt2); 32 | 33 | reg_opts = section.get_registered_options(); 34 | REQUIRE(reg_opts.size() == 1); 35 | CHECK(reg_opts.back() == intopt2); 36 | section.unregister_option(intopt2); 37 | CHECK(section.get_registered_options().empty()); 38 | 39 | section.register_new_option(intopt); 40 | section.priv->xml = (xmlNode*)0x123; 41 | auto clone = section.clone_with_name("Cloned_Section"); 42 | CHECK(clone->get_name() == "Cloned_Section"); 43 | CHECK(clone->priv->xml == (xmlNode*)0x123); 44 | CHECK(clone->get_option_or("IntOption") != intopt); 45 | CHECK(clone->get_option_or("IntOption")->get_name() == intopt->get_name()); 46 | CHECK(clone->get_option_or( 47 | "IntOption")->get_value_str() == intopt->get_value_str()); 48 | } 49 | -------------------------------------------------------------------------------- /include/wayfire/config/config-manager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace wf 6 | { 7 | namespace config 8 | { 9 | /** 10 | * Manages the whole configuration of a program. 11 | * The configuration consists of a list of sections with their options. 12 | */ 13 | class config_manager_t 14 | { 15 | public: 16 | /** 17 | * Add the given config section to the configuration. 18 | * If the section already exists, each new option will be added to the 19 | * existing section, and already existing options will be overwritten. 20 | * 21 | * @param section The section to add, must be non-null. 22 | */ 23 | void merge_section(std::shared_ptr section); 24 | 25 | /** 26 | * Find the configuration section with the given name. 27 | * @return nullptr if the section doesn't exist. 28 | */ 29 | std::shared_ptr get_section(const std::string& name) const; 30 | 31 | /** 32 | * @return A list of all sections currently in the config manager. 33 | */ 34 | std::vector> get_all_sections() const; 35 | 36 | /** 37 | * Get the option with the given name. 38 | * The name consists of the name of the option section, followed by a '/', 39 | * then followed by the actual name of the option in the option section, 40 | * for example 'core/plugins'. 41 | * 42 | * If the option doesn't exist, nullptr is returned. 43 | */ 44 | std::shared_ptr get_option(const std::string& name) const; 45 | 46 | /** 47 | * Get the option with the given name. Same semantics as 48 | * get_option(std::string), but casts the result to the appropriate type. 49 | */ 50 | template 51 | std::shared_ptr> get_option(const std::string& name) const 52 | { 53 | return std::dynamic_pointer_cast>(get_option(name)); 54 | } 55 | 56 | config_manager_t(); 57 | config_manager_t(config_manager_t&& other); 58 | config_manager_t& operator =(config_manager_t&& other); 59 | 60 | virtual ~config_manager_t(); 61 | 62 | private: 63 | struct impl; 64 | std::unique_ptr priv; 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /include/wayfire/config/section.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace wf 8 | { 9 | namespace config 10 | { 11 | /** 12 | * Represents a section in the config file. 13 | * Each section has a list of options. 14 | */ 15 | class section_t 16 | { 17 | public: 18 | /** 19 | * Create a new empty section. 20 | * 21 | * The section name can be arbitrary, however when reading config from a 22 | * file there are additional restrictions. 23 | */ 24 | section_t(const std::string& name); 25 | virtual ~section_t(); 26 | 27 | /** @return The name of the config section. */ 28 | std::string get_name() const; 29 | 30 | /** @return A deep copy of the config section with a new name. */ 31 | std::shared_ptr clone_with_name(const std::string name) const; 32 | 33 | /** 34 | * @return The option with the given name, or nullptr if no such option 35 | * has been added yet. 36 | */ 37 | std::shared_ptr get_option_or(const std::string& name); 38 | 39 | /** 40 | * @return The option with the given name. 41 | * @throws std::invalid_argument if the option hasn't been added. 42 | */ 43 | std::shared_ptr get_option(const std::string& name); 44 | 45 | using option_list_t = std::vector>; 46 | /** 47 | * @return A list of all available options in this config section. 48 | */ 49 | option_list_t get_registered_options() const; 50 | 51 | /** 52 | * Register a new option, which means it is marked as belonging to this 53 | * section and it will show up in the list of get_registered_options(). 54 | * 55 | * If an option with the same name already exists, it will be overwritten. 56 | */ 57 | void register_new_option(std::shared_ptr option); 58 | 59 | /** 60 | * Remove an option from the registered options in this section. 61 | * No-op if the option is not part of the section, or if option is null. 62 | */ 63 | void unregister_option(std::shared_ptr option); 64 | 65 | struct impl; 66 | std::unique_ptr priv; 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/option_wrapper_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | static wf::config::config_manager_t config; 8 | 9 | template 10 | class wrapper_t : public wf::base_option_wrapper_t 11 | { 12 | public: 13 | wrapper_t() : wf::base_option_wrapper_t() 14 | {} 15 | wrapper_t(const std::string& option) : 16 | wf::base_option_wrapper_t() 17 | { 18 | this->load_option(option); 19 | } 20 | 21 | protected: 22 | std::shared_ptr load_raw_option( 23 | const std::string& name) override 24 | { 25 | return config.get_option(name); 26 | } 27 | }; 28 | 29 | TEST_CASE("wf::base_option_wrapper_t") 30 | { 31 | using namespace wf; 32 | using namespace wf::config; 33 | 34 | auto section = std::make_shared("Test"); 35 | auto opt = std::make_shared>("Option1", 5); 36 | 37 | compound_option_t::entries_t entries; 38 | entries.push_back(std::make_unique>("hey_")); 39 | entries.push_back(std::make_unique>("bey_")); 40 | 41 | auto coptr = new compound_option_t{"Option2", std::move(entries)}; 42 | auto copt = std::shared_ptr(coptr); 43 | 44 | section->register_new_option(opt); 45 | section->register_new_option(copt); 46 | ::config.merge_section(section); 47 | 48 | wrapper_t wrapper{"Test/Option1"}; 49 | CHECK((option_sptr_t)wrapper == opt); 50 | CHECK(wrapper == 5); 51 | 52 | wrapper_t> wrapper2{"Test/Option2"}; 53 | CHECK((std::shared_ptr)wrapper2 == copt); 54 | bool value_in_compound_list_is_ok = 55 | (wrapper2.value() == compound_list_t{}); 56 | CHECK(value_in_compound_list_is_ok); 57 | 58 | bool updated = false; 59 | wrapper.set_callback([&] () 60 | { 61 | updated = true; 62 | }); 63 | opt->set_value(6); 64 | CHECK(updated); 65 | 66 | /* Check move operations */ 67 | wrapper_t wrapper1{"Test/Option1"}; 68 | CHECK((option_sptr_t)wrapper1 == opt); 69 | } 70 | -------------------------------------------------------------------------------- /test/option_base_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include 4 | 5 | class option_base_stub_t : public wf::config::option_base_t 6 | { 7 | public: 8 | option_base_stub_t(std::string name) : 9 | option_base_t(name) 10 | {} 11 | 12 | std::shared_ptr clone_option() const override 13 | { 14 | return nullptr; 15 | } 16 | 17 | bool set_default_value_str(const std::string&) override 18 | { 19 | return true; 20 | } 21 | 22 | bool set_value_str(const std::string&) override 23 | { 24 | return false; 25 | } 26 | 27 | void reset_to_default() override 28 | {} 29 | std::string get_value_str() const override 30 | { 31 | return ""; 32 | } 33 | 34 | std::string get_default_value_str() const override 35 | { 36 | return ""; 37 | } 38 | 39 | public: 40 | void notify_updated() const 41 | { 42 | option_base_t::notify_updated(); 43 | } 44 | }; 45 | 46 | 47 | TEST_CASE("wf::option_base_t") 48 | { 49 | option_base_stub_t option{"string"}; 50 | CHECK(option.get_name() == "string"); 51 | 52 | int callback_called = 0; 53 | int callback2_called = 0; 54 | 55 | wf::config::option_base_t::updated_callback_t callback, callback2; 56 | callback = [&] () { callback_called++; }; 57 | callback2 = [&] () { callback2_called++; }; 58 | 59 | option.add_updated_handler(&callback); 60 | option.notify_updated(); 61 | CHECK(callback_called == 1); 62 | CHECK(callback2_called == 0); 63 | 64 | option.add_updated_handler(&callback); 65 | option.add_updated_handler(&callback2); 66 | option.notify_updated(); 67 | CHECK(callback_called == 3); 68 | CHECK(callback2_called == 1); 69 | 70 | option.rem_updated_handler(&callback); 71 | option.notify_updated(); 72 | CHECK(callback_called == 3); 73 | CHECK(callback2_called == 2); 74 | 75 | option.rem_updated_handler(&callback2); 76 | option.notify_updated(); 77 | CHECK(callback_called == 3); 78 | CHECK(callback2_called == 2); 79 | 80 | option.set_locked(); 81 | CHECK(option.is_locked()); 82 | option.set_locked(); 83 | option.set_locked(false); 84 | CHECK(option.is_locked()); 85 | option.set_locked(false); 86 | CHECK(option.is_locked() == false); 87 | } 88 | -------------------------------------------------------------------------------- /src/section.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "section-impl.hpp" 3 | 4 | wf::config::section_t::section_t(const std::string& name) 5 | { 6 | this->priv = std::make_unique(); 7 | this->priv->name = name; 8 | } 9 | 10 | wf::config::section_t::~section_t() = default; 11 | 12 | std::string wf::config::section_t::get_name() const 13 | { 14 | return this->priv->name; 15 | } 16 | 17 | std::shared_ptr wf::config::section_t::clone_with_name( 18 | const std::string name) const 19 | { 20 | auto result = std::make_shared(name); 21 | for (auto& option : priv->options) 22 | { 23 | result->register_new_option(option.second->clone_option()); 24 | } 25 | 26 | result->priv->xml = this->priv->xml; 27 | return result; 28 | } 29 | 30 | std::shared_ptr wf::config::section_t::get_option_or( 31 | const std::string& name) 32 | { 33 | if (this->priv->options.count(name)) 34 | { 35 | return this->priv->options[name]; 36 | } 37 | 38 | return nullptr; 39 | } 40 | 41 | std::shared_ptr wf::config::section_t::get_option( 42 | const std::string& name) 43 | { 44 | auto option = get_option_or(name); 45 | if (!option) 46 | { 47 | throw std::invalid_argument("Non-existing option " + name + 48 | " in config section " + this->get_name()); 49 | } 50 | 51 | return option; 52 | } 53 | 54 | wf::config::section_t::option_list_t wf::config::section_t::get_registered_options() 55 | const 56 | { 57 | option_list_t list; 58 | for (auto& option : priv->options) 59 | { 60 | list.push_back(option.second); 61 | } 62 | 63 | return list; 64 | } 65 | 66 | void wf::config::section_t::register_new_option( 67 | std::shared_ptr option) 68 | { 69 | if (!option) 70 | { 71 | throw std::invalid_argument( 72 | "Cannot add null option to section " + this->get_name()); 73 | } 74 | 75 | this->priv->options[option->get_name()] = option; 76 | } 77 | 78 | void wf::config::section_t::unregister_option( 79 | std::shared_ptr option) 80 | { 81 | if (!option) 82 | { 83 | return; 84 | } 85 | 86 | auto it = this->priv->options.find(option->get_name()); 87 | if ((it != this->priv->options.end()) && (it->second == option)) 88 | { 89 | this->priv->options.erase(it); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | types_test = executable( 2 | 'types_test', 3 | 'types_test.cpp', 4 | dependencies: [wfconfig, doctest], 5 | install: false) 6 | test('Types test', types_test) 7 | 8 | option_base_test = executable( 9 | 'option_base_test', 10 | 'option_base_test.cpp', 11 | dependencies: [wfconfig, doctest], 12 | install: false) 13 | test('OptionBase test', option_base_test) 14 | 15 | option_test = executable( 16 | 'option_test', 17 | 'option_test.cpp', 18 | dependencies: [wfconfig, doctest, libxml2], 19 | install: false) 20 | test('Option test', option_test) 21 | 22 | option_wrapper_test = executable( 23 | 'option_wrapper_test', 24 | 'option_wrapper_test.cpp', 25 | dependencies: [wfconfig, doctest], 26 | install: false) 27 | test('Option wrapper test', option_wrapper_test) 28 | 29 | section_test = executable( 30 | 'section_test', 31 | 'section_test.cpp', 32 | dependencies: [wfconfig, doctest, libxml2], 33 | install: false) 34 | test('Section test', section_test) 35 | 36 | xml_test = executable( 37 | 'xml_test', 38 | 'xml_test.cpp', 39 | include_directories: wfconfig_inc, 40 | dependencies: [wfconfig, doctest, libxml2], 41 | install: false) 42 | test('XML test', xml_test) 43 | 44 | config_manager_test = executable( 45 | 'config_manager_test', 46 | 'config_manager_test.cpp', 47 | dependencies: [wfconfig, doctest], 48 | install: false) 49 | test('ConfigManager test', config_manager_test) 50 | 51 | file_parse_test = executable( 52 | 'file_test', 53 | 'file_test.cpp', 54 | dependencies: [wfconfig, doctest, libxml2], 55 | install: false, 56 | cpp_args: '-DTEST_SOURCE="' + meson.current_source_dir() + '"') 57 | test('File parsing test', file_parse_test) 58 | 59 | # Utils 60 | log_test = executable( 61 | 'log_test', 62 | 'log_test.cpp', 63 | dependencies: [wfconfig, doctest], 64 | install: false) 65 | test('Log test', log_test) 66 | 67 | duration_test = executable( 68 | 'duration_test', 69 | 'duration_test.cpp', 70 | dependencies: [wfconfig, doctest], 71 | install: false) 72 | test('Duration test', duration_test) 73 | 74 | number_locale_test = executable( 75 | 'number_locale_test', 76 | 'number_locale_test.cpp', 77 | dependencies: [wfconfig, doctest], 78 | install: false) 79 | # enable locale test only on request 80 | if get_option('locale_test') 81 | test('Number locale test', number_locale_test) 82 | endif 83 | -------------------------------------------------------------------------------- /include/wayfire/config/xml.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | /** 3 | * This file contains definitions related to parsing XML files and turning them 4 | * into config options and sections. 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | namespace wf 14 | { 15 | namespace config 16 | { 17 | namespace xml 18 | { 19 | /** 20 | * Create a new option from the given data in the xmlNode. 21 | * Errors are printed to the log (see wayfire/util/log.hpp). 22 | * 23 | * If the operation is successful, the xmlNodePtr should not be freed, because 24 | * an internal reference will be taken. 25 | * 26 | * @param node The xmlNode which corresponds to the option. 27 | * The attributes used are name and type, as well as the child. 28 | * 29 | * @return The generated option, if the xmlNode contained a valid option, and 30 | * nullptr otherwise. The dynamic type of the option is 31 | * wf::config::option_t, depending on the type attribute of the xmlNode. 32 | */ 33 | std::shared_ptr create_option_from_xml_node( 34 | xmlNodePtr node); 35 | 36 | /** 37 | * Create a new section from the given xmlNode and add each successfully parsed 38 | * child as an option in the section. 39 | * Errors are printed to the log (see wayfire/util/log.hpp). 40 | * 41 | * If the operation is successful, the xmlNodePtr should not be freed, because 42 | * an internal reference will be taken. 43 | * 44 | * @param node the xmlNode which corresponds to the section node. The only 45 | * attribute of @node used is name, and it determines the name of the generated 46 | * section. 47 | * 48 | * @return nullptr if section name is missing from the xmlNode, and the 49 | * generated config section otherwise. 50 | */ 51 | std::shared_ptr create_section_from_xml_node(xmlNodePtr node); 52 | 53 | /** 54 | * Get the XML node which was used to create @option with 55 | * create_option_from_xml_node. 56 | * 57 | * @return The xmlNodePtr or NULL if the option wasn't created from an xml node. 58 | */ 59 | xmlNodePtr get_option_xml_node( 60 | std::shared_ptr option); 61 | 62 | /** 63 | * Get the XML node which was used to create @section with 64 | * create_section_from_xml_node. 65 | * 66 | * @return The xmlNodePtr or NULL if the section wasn't created from an xml 67 | * node. 68 | */ 69 | xmlNodePtr get_section_xml_node(std::shared_ptr section); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /include/wayfire/util/log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * Utilities for logging to a selected output stream. 5 | */ 6 | #include 7 | 8 | namespace wf 9 | { 10 | namespace log 11 | { 12 | enum log_level_t 13 | { 14 | /** Lowest level, gray if colors are enabled. */ 15 | LOG_LEVEL_DEBUG = 0, 16 | /** Information level, no coloring. */ 17 | LOG_LEVEL_INFO = 1, 18 | /** Warning level, yellow/orange. */ 19 | LOG_LEVEL_WARN = 2, 20 | /** Error level, red. */ 21 | LOG_LEVEL_ERROR = 3, 22 | }; 23 | 24 | enum color_mode_t 25 | { 26 | /** Color codes are on */ 27 | LOG_COLOR_MODE_ON = 1, 28 | /** Color codes are off */ 29 | LOG_COLOR_MODE_OFF = 2, 30 | }; 31 | 32 | /** 33 | * (Re-)Initialize the logging system. 34 | * The log output after this call will go to the indicated output stream. 35 | * 36 | * @param minimum_level The minimum severity that a log message needs to have so 37 | * that it can get published. 38 | * 39 | * @param color_mode Whether to enable coloring of the output. 40 | * 41 | * @param strip_path The prefix of file path to strip when debugging with the 42 | * helper macros LOG(D,I,W,E) 43 | */ 44 | void initialize_logging(std::ostream& output_stream, log_level_t minimum_level, 45 | color_mode_t color_mode, std::string strip_path = ""); 46 | 47 | /** 48 | * Log a plain message to the given output stream. 49 | * The output format is: 50 | * 51 | * LL DD-MM-YY HH:MM:SS.MSS - [source:line] message 52 | * 53 | * @param level The log level of the passed message. 54 | * @param contents The message to be printed. 55 | * @param source The file where the message originates from. The prefix 56 | * strip_path specified in initialize_logging will be removed, if it exists. 57 | * If source is empty, no file/line information will be printed. 58 | * @param line The line number of @source 59 | */ 60 | void log_plain(log_level_t level, const std::string& contents, 61 | const std::string& source = "", int line = 0); 62 | } 63 | } 64 | 65 | /** 66 | * A convenience wrapper around log_plain 67 | */ 68 | #define LOG(level, ...) \ 69 | wf::log::log_plain(level, \ 70 | wf::log::detail::format_concat(__VA_ARGS__), __FILE__, __LINE__) 71 | 72 | /** Log a debug message */ 73 | #define LOGD(...) LOG(wf::log::LOG_LEVEL_DEBUG, __VA_ARGS__) 74 | /** Log an info message */ 75 | #define LOGI(...) LOG(wf::log::LOG_LEVEL_INFO, __VA_ARGS__) 76 | /** Log a warning message */ 77 | #define LOGW(...) LOG(wf::log::LOG_LEVEL_WARN, __VA_ARGS__) 78 | /** Log an error message */ 79 | #define LOGE(...) LOG(wf::log::LOG_LEVEL_ERROR, __VA_ARGS__) 80 | -------------------------------------------------------------------------------- /src/config-manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct wf::config::config_manager_t::impl 6 | { 7 | std::map> sections; 8 | }; 9 | 10 | void wf::config::config_manager_t::merge_section( 11 | std::shared_ptr section) 12 | { 13 | assert(section); 14 | if (this->priv->sections.count(section->get_name()) == 0) 15 | { 16 | /* Did not exist previously, just add the new section */ 17 | this->priv->sections[section->get_name()] = section; 18 | return; 19 | } 20 | 21 | /* Merge with existing config section */ 22 | auto existing_section = get_section(section->get_name()); 23 | auto merging_options = section->get_registered_options(); 24 | for (auto& option : merging_options) 25 | { 26 | auto existing_option = 27 | existing_section->get_option_or(option->get_name()); 28 | 29 | if (existing_option) 30 | { 31 | existing_option->set_value_str(option->get_value_str()); 32 | } else 33 | { 34 | existing_section->register_new_option(option); 35 | } 36 | } 37 | } 38 | 39 | std::shared_ptr wf::config::config_manager_t::get_section( 40 | const std::string& name) const 41 | { 42 | if (this->priv->sections.count(name)) 43 | { 44 | return this->priv->sections.at(name); 45 | } 46 | 47 | return nullptr; 48 | } 49 | 50 | std::vector> wf::config::config_manager_t::get_all_sections() const 51 | { 52 | std::vector> list; 53 | for (auto& section : this->priv->sections) 54 | { 55 | list.push_back(section.second); 56 | } 57 | 58 | return list; 59 | } 60 | 61 | std::shared_ptr wf::config::config_manager_t::get_option( 62 | const std::string& name) const 63 | { 64 | size_t splitter = name.find_first_of("/"); 65 | if (splitter == std::string::npos) 66 | { 67 | return nullptr; 68 | } 69 | 70 | auto section_name = name.substr(0, splitter); 71 | auto option_name = name.substr(splitter + 1); 72 | if (section_name.empty() || option_name.empty()) 73 | { 74 | return nullptr; 75 | } 76 | 77 | auto section_ptr = get_section(section_name); 78 | if (section_ptr) 79 | { 80 | return section_ptr->get_option_or(option_name); 81 | } 82 | 83 | return nullptr; 84 | } 85 | 86 | wf::config::config_manager_t::config_manager_t() 87 | { 88 | this->priv = std::make_unique(); 89 | } 90 | 91 | /** Default move operations */ 92 | wf::config::config_manager_t::config_manager_t( 93 | config_manager_t&& other) = default; 94 | wf::config::config_manager_t& wf::config::config_manager_t::operator =( 95 | config_manager_t&& other) = default; 96 | 97 | wf::config::config_manager_t::~config_manager_t() = default; 98 | -------------------------------------------------------------------------------- /test/config_manager_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | TEST_CASE("wf::config::config_manager_t") 9 | { 10 | using namespace wf; 11 | using namespace wf::config; 12 | using namespace option_type; 13 | 14 | config_manager_t config{}; 15 | auto expect_sections = [&] (std::set names) 16 | { 17 | auto all = config.get_all_sections(); 18 | CHECK(all.size() == names.size()); 19 | 20 | std::set present; 21 | std::transform(all.begin(), all.end(), 22 | std::inserter(present, present.end()), 23 | [] (const auto& section) { return section->get_name(); }); 24 | CHECK(present == names); 25 | }; 26 | 27 | expect_sections({}); 28 | 29 | CHECK(config.get_option("no_such_option") == nullptr); 30 | CHECK(config.get_option("section/nonexistent") == nullptr); 31 | CHECK(config.get_option("section/nonexist/ent") == nullptr); 32 | 33 | CHECK(config.get_section("FirstSection") == nullptr); 34 | config.merge_section(std::make_shared("FirstSection")); 35 | expect_sections({"FirstSection"}); 36 | 37 | auto section = config.get_section("FirstSection"); 38 | REQUIRE(section != nullptr); 39 | CHECK(section->get_name() == "FirstSection"); 40 | CHECK(section->get_registered_options().empty()); 41 | 42 | CHECK(config.get_option("FirstSection/FirstOption") == nullptr); 43 | 44 | auto color = option_type::from_string("#FFFF").value(); 45 | auto option = std::make_shared>("ColorOption", color); 46 | section->register_new_option(option); 47 | 48 | CHECK(config.get_option( 49 | "FirstSection/ColorOption")->get_value() == color); 50 | CHECK(config.get_option("FirstSection/ColorOption") == option); 51 | 52 | auto section2 = config.get_section("SecondSection"); 53 | CHECK(section2 == nullptr); 54 | 55 | auto section_overwrite = std::make_shared("FirstSection"); 56 | section_overwrite->register_new_option( 57 | std::make_shared>( 58 | "ColorOption", from_string("#CCCC").value())); 59 | section_overwrite->register_new_option( 60 | std::make_shared>("IntOption", 5)); 61 | 62 | section2 = std::make_shared("SecondSection"); 63 | section2->register_new_option( 64 | std::make_shared>("IntOption", 6)); 65 | 66 | config.merge_section(section_overwrite); 67 | CHECK(config.get_section("FirstSection") == section); // do not overwrite 68 | expect_sections({"FirstSection"}); 69 | 70 | auto stored_color_opt = 71 | config.get_option("FirstSection/ColorOption"); 72 | REQUIRE(stored_color_opt != nullptr); 73 | CHECK(stored_color_opt->get_value_str() == "#CCCCCCCC"); 74 | 75 | auto stored_int_opt = 76 | config.get_option("FirstSection/IntOption"); 77 | REQUIRE(stored_int_opt); 78 | CHECK(stored_int_opt->get_value_str() == "5"); 79 | 80 | config.merge_section(section2); 81 | expect_sections({"FirstSection", "SecondSection"}); 82 | 83 | stored_int_opt = config.get_option("FirstSection/IntOption"); 84 | REQUIRE(stored_int_opt); 85 | CHECK(stored_int_opt->get_value_str() == "5"); // remains same 86 | 87 | stored_int_opt = config.get_option("SecondSection/IntOption"); 88 | REQUIRE(stored_int_opt); 89 | CHECK(stored_int_opt->get_value_str() == "6"); 90 | } 91 | -------------------------------------------------------------------------------- /include/wayfire/config/file.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace wf 6 | { 7 | namespace config 8 | { 9 | /** 10 | * Parse a multi-line string as a configuration file. 11 | * The string consists of multiple sections of the following format: 12 | * 13 | * [section_name] 14 | * option1 = value1 15 | * option2 = value2 16 | * ... 17 | * 18 | * Blank lines and whitespace characters around the '=' sign are ignored, as 19 | * well as whitespaces at the beginning or at the end of each line. 20 | * 21 | * When a line contains a '#', the line from this point on is considered a 22 | * comment except when it is immediately preceded by a '\'. 23 | * 24 | * When a line ends in '\', it automatically joined with the next line, except 25 | * if it isn't escaped with another '\'. 26 | * 27 | * Each valid parsed option is used to set the value of the corresponding option 28 | * in @manager. Each line which contains errors is reported on the log and then 29 | * ignored. 30 | * 31 | * @param manager The config manager to update. 32 | * @param source The multi-line string representing the source 33 | * @param source_name The name to be used when reporting errors to the log 34 | */ 35 | void load_configuration_options_from_string(config_manager_t& manager, 36 | const std::string& source, const std::string& source_name = ""); 37 | 38 | /** 39 | * Create a string which conttains all the sections and the options in the given 40 | * configuration manager. The format is the same one as the one described in 41 | * load_configuration_options_from_string() 42 | * 43 | * @return The string representation of the config manager. 44 | */ 45 | std::string save_configuration_options_to_string( 46 | const config_manager_t& manager); 47 | 48 | /** 49 | * Load the options from the given config file. 50 | * 51 | * This is roughly equivalent to reading the file to a string, and then calling 52 | * load_configuration_options_from_string(), but this function also tries to get 53 | * a shared lock on the config file, and does not do anything if another process 54 | * already holds an exclusive lock. 55 | * 56 | * @param manager The config manager to update. 57 | * @param file The config file to use. 58 | * 59 | * @return True if the config file was reloaded, false if file could not be 60 | * opened or a lock could not be acquired. 61 | */ 62 | bool load_configuration_options_from_file(config_manager_t& manager, 63 | const std::string& file); 64 | 65 | /** 66 | * Writes the options in the given configuration to the given file. 67 | * It is roughly equivalent to calling serialize_configuration_manager() and 68 | * then replacing the file contents with the resulting string, but this function 69 | * waits until it can get an exclusive lock on the config file. 70 | */ 71 | void save_configuration_to_file(const config_manager_t& manager, 72 | const std::string& file); 73 | 74 | /** 75 | * Build a configuration for the given program from the files on the filesystem. 76 | * 77 | * The following steps are performed: 78 | * 1. Each of the XML files in each of @xmldirs are read, and options there 79 | * are used to build a configuration. Note that the XML nodes which are 80 | * allocated will not be freed. 81 | * 2. The @sysconf file is used to overwrite default values from XML files. 82 | * 3. The @userconf file is used to determine the actual values of options. 83 | * 84 | * If any of the steps results in an error, the error will be reported to the 85 | * command line and the process will continue. 86 | */ 87 | config_manager_t build_configuration(const std::vector& xmldirs, 88 | const std::string& sysconf, const std::string& userconf); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/log_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | struct test_struct 8 | { 9 | int x, y; 10 | }; 11 | 12 | namespace wf 13 | { 14 | namespace log 15 | { 16 | template<> 17 | std::string to_string(test_struct str) 18 | { 19 | return "(" + std::to_string(str.x) + "," + std::to_string(str.y) + ")"; 20 | } 21 | } 22 | } 23 | 24 | TEST_CASE("wf::log::detail::format_concat()") 25 | { 26 | using namespace wf::log; 27 | 28 | CHECK(detail::format_concat("test", 123) == "test123"); 29 | CHECK(detail::format_concat("test ", 123, " ", false, 30 | true) == "test 123 falsetrue"); 31 | int *p = (int*)0xfff; 32 | int *null = nullptr; 33 | CHECK(detail::format_concat("test ", p) == "test 0xfff"); 34 | CHECK(detail::format_concat("test ", null) == "test (null)"); 35 | CHECK(detail::format_concat("$", test_struct{1, 2}, "$") == "$(1,2)$"); 36 | 37 | char *t = nullptr; 38 | CHECK(detail::format_concat(t, "$") == "(null)$"); 39 | } 40 | 41 | TEST_CASE("wf::log::log_plain()") 42 | { 43 | using namespace wf::log; 44 | std::stringstream out; 45 | 46 | auto check_line = [&out] (std::string expect) 47 | { 48 | std::string line; 49 | std::getline(out, line); 50 | 51 | /* Remove date and current time, because it isn't reproducible. */ 52 | int time_start_index = 2; 53 | int time_length = 1 + 10 + 1 + 12; /* space + date + space + time */ 54 | 55 | REQUIRE(line.length() >= time_start_index + time_length); 56 | line.erase(time_start_index, time_length); 57 | 58 | CHECK(line == expect); 59 | }; 60 | 61 | initialize_logging(out, LOG_LEVEL_DEBUG, LOG_COLOR_MODE_OFF, "/test/strip/"); 62 | 63 | log_plain(LOG_LEVEL_DEBUG, "Test log", "/test/strip/main.cpp", 5); 64 | check_line("DD - [main.cpp:5] Test log"); 65 | 66 | log_plain(LOG_LEVEL_INFO, "Test no source"); 67 | check_line("II - Test no source"); 68 | 69 | log_plain(LOG_LEVEL_INFO, "Test log", "/test/strip/main.cpp", 56789); 70 | check_line("II - [main.cpp:56789] Test log"); 71 | 72 | log_plain(LOG_LEVEL_WARN, "Test log", "test/strip/main.cpp", 5); 73 | check_line("WW - [test/strip/main.cpp:5] Test log"); 74 | 75 | log_plain(LOG_LEVEL_ERROR, "Test error", "/test/strip//test/strip/main.cpp", 5); 76 | check_line("EE - [/test/strip/main.cpp:5] Test error"); 77 | 78 | initialize_logging(out, LOG_LEVEL_ERROR, LOG_COLOR_MODE_OFF, "/test/strip/"); 79 | 80 | /* Ignore non-error messages */ 81 | log_plain(LOG_LEVEL_WARN, "Test log", "test/strip/main.cpp", 5); 82 | log_plain(LOG_LEVEL_DEBUG, "Test log", "/test/strip/main.cpp", 5); 83 | log_plain(LOG_LEVEL_INFO, "Test log", "/test/strip/main.cpp", 56789); 84 | /* Show just errors */ 85 | log_plain(LOG_LEVEL_ERROR, "Test error", "main.cpp", 5); 86 | check_line("EE - [main.cpp:5] Test error"); 87 | 88 | /* Stream shouldn't have any more characters */ 89 | char dummy; 90 | out >> dummy; 91 | CHECK(out.eof()); 92 | } 93 | 94 | TEST_CASE("wf::log::log_plain(color_on)") 95 | { 96 | using namespace wf::log; 97 | std::stringstream stream; 98 | initialize_logging(stream, LOG_LEVEL_DEBUG, LOG_COLOR_MODE_ON); 99 | 100 | auto check_line = [&stream] (std::string set_color) 101 | { 102 | std::string line; 103 | std::getline(stream, line); 104 | /* Check that line begins with the proper color code and ends with 105 | * color reset */ 106 | 107 | const std::string reset = "\033[0m"; 108 | 109 | REQUIRE(line.length() >= set_color.length() + reset.length()); 110 | 111 | CHECK(line.find(set_color) == 0); 112 | CHECK(line.rfind(reset) == line.length() - reset.length()); 113 | }; 114 | 115 | LOGD("test"); 116 | check_line("\033[0m"); 117 | LOGI("test"); 118 | check_line("\033[0;34m"); 119 | LOGW("test"); 120 | check_line("\033[0;33m"); 121 | LOGE("test"); 122 | check_line("\033[1;31m"); 123 | } 124 | -------------------------------------------------------------------------------- /src/log.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | template<> 10 | std::string wf::log::to_string(void *arg) 11 | { 12 | if (!arg) 13 | { 14 | return "(null)"; 15 | } 16 | 17 | std::ostringstream out; 18 | out << arg; 19 | return out.str(); 20 | } 21 | 22 | template<> 23 | std::string wf::log::to_string(bool arg) 24 | { 25 | return arg ? "true" : "false"; 26 | } 27 | 28 | /** 29 | * A singleton to hold log configuration. 30 | */ 31 | struct log_global_t 32 | { 33 | std::reference_wrapper out = std::ref(std::cout); 34 | 35 | wf::log::log_level_t level = wf::log::LOG_LEVEL_INFO; 36 | wf::log::color_mode_t color_mode = wf::log::LOG_COLOR_MODE_OFF; 37 | std::string strip_path = ""; 38 | 39 | std::string clear_color = ""; 40 | 41 | static log_global_t& get() 42 | { 43 | static log_global_t instance; 44 | return instance; 45 | } 46 | 47 | private: 48 | log_global_t() 49 | {} 50 | }; 51 | 52 | void wf::log::initialize_logging(std::ostream& output_stream, 53 | log_level_t minimum_level, color_mode_t color_mode, std::string strip_path) 54 | { 55 | auto& state = log_global_t::get(); 56 | state.out = std::ref(output_stream); 57 | state.level = minimum_level; 58 | state.color_mode = color_mode; 59 | state.strip_path = strip_path; 60 | 61 | if (state.color_mode == LOG_COLOR_MODE_ON) 62 | { 63 | state.clear_color = "\033[0m"; 64 | } else 65 | { 66 | state.clear_color = ""; 67 | } 68 | } 69 | 70 | /** Get the line prefix for the given log level */ 71 | static std::string get_level_prefix(wf::log::log_level_t level) 72 | { 73 | bool color = log_global_t::get().color_mode == wf::log::LOG_COLOR_MODE_ON; 74 | static std::map color_codes = 75 | { 76 | {wf::log::LOG_LEVEL_DEBUG, "\033[0m"}, 77 | {wf::log::LOG_LEVEL_INFO, "\033[0;34m"}, 78 | {wf::log::LOG_LEVEL_WARN, "\033[0;33m"}, 79 | {wf::log::LOG_LEVEL_ERROR, "\033[1;31m"}, 80 | }; 81 | 82 | static std::map line_prefix = 83 | { 84 | {wf::log::LOG_LEVEL_DEBUG, "DD"}, 85 | {wf::log::LOG_LEVEL_INFO, "II"}, 86 | {wf::log::LOG_LEVEL_WARN, "WW"}, 87 | {wf::log::LOG_LEVEL_ERROR, "EE"}, 88 | }; 89 | 90 | if (color) 91 | { 92 | return color_codes[level] + line_prefix[level]; 93 | } 94 | 95 | return line_prefix[level]; 96 | } 97 | 98 | /** Get the current time and date in a suitable format. */ 99 | static std::string get_formatted_date_time() 100 | { 101 | using namespace std::chrono; 102 | auto now = system_clock::now(); 103 | auto tt = system_clock::to_time_t(now); 104 | auto ms = duration_cast(now.time_since_epoch()) % 1000; 105 | 106 | std::ostringstream out; 107 | 108 | struct tm buffer; 109 | localtime_r(&tt, &buffer); 110 | out << std::put_time(&buffer, "%Y-%m-%d %H:%M:%S."); 111 | out << std::setfill('0') << std::setw(3) << ms.count(); 112 | return out.str(); 113 | } 114 | 115 | /** Strip the strip_path from the given path. */ 116 | static std::string strip_path(const std::string& path) 117 | { 118 | auto prefix = log_global_t::get().strip_path; 119 | if ((prefix.length() > 0) && (path.find(prefix) == 0)) 120 | { 121 | return path.substr(prefix.length()); 122 | } 123 | 124 | std::string skip_chars = "./"; 125 | size_t idx = path.find_first_not_of(skip_chars); 126 | if (idx != std::string::npos) 127 | { 128 | return path.substr(idx); 129 | } 130 | 131 | return path; 132 | } 133 | 134 | /** 135 | * Log a plain message to the given output stream. 136 | * The output format is: 137 | * 138 | * LL DD-MM-YY HH:MM:SS.MSS - [source:line] message 139 | * 140 | * @param level The log level of the passed message. 141 | * @param contents The message to be printed. 142 | * @param source The file where the message originates from. The prefix 143 | * strip_path specified in initialize_logging will be removed, if it exists. 144 | * @param line The line number of @source 145 | */ 146 | void wf::log::log_plain(log_level_t level, const std::string& contents, 147 | const std::string& source, int line_nr) 148 | { 149 | auto& state = log_global_t::get(); 150 | if (state.level > level) 151 | { 152 | return; 153 | } 154 | 155 | std::string path_info; 156 | if (!source.empty()) 157 | { 158 | path_info = wf::log::detail::format_concat( 159 | "[", strip_path(source), ":", line_nr, "] "); 160 | } 161 | 162 | state.out.get() << 163 | wf::log::detail::format_concat( 164 | get_level_prefix(level), " ", 165 | get_formatted_date_time(), 166 | " - ", path_info, contents) << 167 | state.clear_color << std::endl; 168 | } 169 | -------------------------------------------------------------------------------- /include/wayfire/nonstd/safe-list.hpp: -------------------------------------------------------------------------------- 1 | #ifndef WF_SAFE_LIST_HPP 2 | #define WF_SAFE_LIST_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace wf 12 | { 13 | /** 14 | * The safe list is a trimmed down version of std::list. 15 | * 16 | * It supports adding an item to the end of a list, iterating over it and erasing elements. 17 | * The important advantage is that elements can be callbacks, which may be executed during the iteration, 18 | * and the callbacks can then add or remove elements from the list safely. 19 | * 20 | * The typical usage of safe list is for bindings and signal handlers. 21 | */ 22 | template 23 | class safe_list_t 24 | { 25 | static_assert(std::is_move_constructible_v>, "T must be moveable!"); 26 | 27 | public: 28 | safe_list_t() 29 | {} 30 | 31 | T& back() 32 | { 33 | auto it = list.rbegin(); 34 | assert((it != list.rend()) && "back() on an empty list!"); 35 | 36 | while (!((*it).has_value())) 37 | { 38 | ++it; 39 | assert((it != list.rend()) && "back() on an empty list!"); 40 | } 41 | 42 | return **it; 43 | } 44 | 45 | size_t size() const 46 | { 47 | if (!is_dirty) 48 | { 49 | return list.size(); 50 | } 51 | 52 | return std::count_if(list.begin(), list.end(), [] (const auto& elem) { return elem.has_value(); }); 53 | } 54 | 55 | /* Push back by copying */ 56 | void push_back(T value) 57 | { 58 | list.push_back({std::move(value)}); 59 | } 60 | 61 | /* Call func for each non-erased element of the list */ 62 | void for_each(std::function func) 63 | { 64 | _start_iter(); 65 | 66 | // Important: make sure we do not iterate over additional values in the list which are added 67 | // afterwards. 68 | size_t size = list.size(); 69 | for (size_t i = 0; i < size; i++) 70 | { 71 | if (list[i]) 72 | { 73 | func(*list[i]); 74 | } 75 | } 76 | 77 | _stop_iter(); 78 | } 79 | 80 | /* Call func for each non-erased element of the list in reversed order */ 81 | void for_each_reverse(std::function func) 82 | { 83 | _start_iter(); 84 | for (size_t i = list.size(); i > 0; i--) 85 | { 86 | if (list[i - 1]) 87 | { 88 | func(*list[i - 1]); 89 | } 90 | } 91 | 92 | _stop_iter(); 93 | } 94 | 95 | /* Safely remove all elements equal to value */ 96 | void remove_all(const T& value) 97 | { 98 | remove_if([=] (const T& el) { return el == value; }); 99 | } 100 | 101 | /* Remove all elements from the list */ 102 | void clear() 103 | { 104 | remove_if([] (const T&) { return true; }); 105 | } 106 | 107 | /* Remove all elements satisfying a given condition. 108 | * This function resets their pointers and scheduling a cleanup operation */ 109 | void remove_if(std::function predicate) 110 | { 111 | _start_iter(); 112 | 113 | const size_t size = list.size(); 114 | for (size_t i = 0; i < size; i++) 115 | { 116 | if (list[i] && predicate(*list[i])) 117 | { 118 | /* First reset the element in the list, and then free resources */ 119 | auto value = std::move(list[i]); 120 | list[i].reset(); 121 | is_dirty = true; 122 | 123 | // Call destructor 124 | value.reset(); 125 | } 126 | } 127 | 128 | _stop_iter(); 129 | _try_cleanup(); 130 | } 131 | 132 | private: 133 | /** 134 | * A vector containing the values of the list. 135 | * To make sure we can iterate over the list and erase any elements from it during iteration, the 'erase' 136 | * operation simply resets the optional value in the list. 137 | * 138 | * After all iterations are done, the list is 'cleaned up', that is, empty elements are removed from it. 139 | */ 140 | std::vector> list; 141 | 142 | int iteration_counter = 0; 143 | bool is_dirty = false; 144 | 145 | /* Remove all invalidated elements in the list */ 146 | void _try_cleanup() 147 | { 148 | if ((iteration_counter > 0) || !is_dirty) 149 | { 150 | // There is an active iteration. 151 | return; 152 | } 153 | 154 | auto it = std::remove_if(list.begin(), list.end(), 155 | [&] (const std::optional& elem) { return !elem.has_value(); }); 156 | list.erase(it, list.end()); 157 | is_dirty = false; 158 | } 159 | 160 | void _start_iter() 161 | { 162 | ++iteration_counter; 163 | } 164 | 165 | void _stop_iter() 166 | { 167 | --iteration_counter; 168 | _try_cleanup(); 169 | } 170 | }; 171 | } 172 | 173 | #endif /* end of include guard: WF_SAFE_LIST_HPP */ 174 | -------------------------------------------------------------------------------- /test/number_locale_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #define WF_CONFIG_DOUBLE_EPS 0.01 11 | 12 | using namespace wf; 13 | using namespace wf::option_type; 14 | 15 | 16 | void setup_test_locale() 17 | { 18 | // requires glibc-langpack-de 19 | // changes std::to_string output 20 | std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); 21 | } 22 | 23 | TEST_CASE("wf::option_type::to_string") 24 | { 25 | setup_test_locale(); 26 | 27 | std::string str = wf::option_type::to_string(3.14); 28 | CHECK(str == "3.140000"); 29 | CHECK(wf::option_type::to_string(3140) == "3140"); 30 | } 31 | 32 | TEST_CASE("wf::bool_wrapper_t locale") 33 | { 34 | setup_test_locale(); 35 | 36 | setup_test_locale(); 37 | CHECK(from_string("True").value()); 38 | setup_test_locale(); 39 | CHECK(from_string("true").value()); 40 | setup_test_locale(); 41 | CHECK(from_string("1").value()); 42 | 43 | setup_test_locale(); 44 | CHECK(from_string("False").value() == false); 45 | setup_test_locale(); 46 | CHECK(from_string("false").value() == false); 47 | setup_test_locale(); 48 | CHECK(from_string("0").value() == false); 49 | setup_test_locale(); 50 | 51 | setup_test_locale(); 52 | CHECK(!from_string("vrai")); 53 | setup_test_locale(); 54 | CHECK(!from_string("faux")); 55 | setup_test_locale(); 56 | 57 | setup_test_locale(); 58 | CHECK(from_string(to_string(true)).value()); 59 | setup_test_locale(); 60 | CHECK(from_string(to_string(false)).value() == false); 61 | setup_test_locale(); 62 | } 63 | 64 | TEST_CASE("wf::int_wrapper_t locale") 65 | { 66 | setup_test_locale(); 67 | 68 | setup_test_locale(); 69 | CHECK(from_string("1456").value() == 1456); 70 | setup_test_locale(); 71 | CHECK(from_string("-89").value() == -89); 72 | 73 | int32_t max = std::numeric_limits::max(); 74 | int32_t min = std::numeric_limits::min(); 75 | setup_test_locale(); 76 | CHECK(from_string(wf::option_type::to_string(max)).value() == max); 77 | setup_test_locale(); 78 | CHECK(from_string(wf::option_type::to_string(min)).value() == min); 79 | 80 | setup_test_locale(); 81 | CHECK(!from_string("1e4")); 82 | setup_test_locale(); 83 | CHECK(!from_string("")); 84 | setup_test_locale(); 85 | CHECK(!from_string("1234567890000")); 86 | 87 | setup_test_locale(); 88 | CHECK(from_string(to_string(456)).value() == 456); 89 | setup_test_locale(); 90 | CHECK(from_string(to_string(0)).value() == 0); 91 | } 92 | 93 | TEST_CASE("wf::double_wrapper_t locale") 94 | { 95 | setup_test_locale(); 96 | CHECK(from_string("0.378000").value() == doctest::Approx(0.378)); 97 | setup_test_locale(); 98 | CHECK(from_string("-89.1847").value() == doctest::Approx(-89.1847)); 99 | 100 | double max = std::numeric_limits::max(); 101 | double min = std::numeric_limits::min(); 102 | 103 | setup_test_locale(); 104 | CHECK(from_string(wf::option_type::to_string( 105 | max)).value() == doctest::Approx(max)); 106 | setup_test_locale(); 107 | CHECK(from_string(wf::option_type::to_string( 108 | min)).value() == doctest::Approx(min)); 109 | 110 | setup_test_locale(); 111 | CHECK(!from_string("1u4")); 112 | setup_test_locale(); 113 | CHECK(!from_string("")); 114 | setup_test_locale(); 115 | CHECK(!from_string("abc")); 116 | 117 | setup_test_locale(); 118 | CHECK(from_string(to_string(-4.56)).value() == 119 | doctest::Approx(-4.56)); 120 | setup_test_locale(); 121 | CHECK(from_string(to_string(0.0)).value() == doctest::Approx(0)); 122 | } 123 | 124 | /* Test that various wf::color_t constructors work */ 125 | TEST_CASE("wf::color_t") 126 | { 127 | using namespace wf; 128 | using namespace option_type; 129 | 130 | setup_test_locale(); 131 | 132 | setup_test_locale(); 133 | CHECK(!from_string("#FFF")); 134 | setup_test_locale(); 135 | CHECK(!from_string("0C1A")); 136 | setup_test_locale(); 137 | CHECK(!from_string("")); 138 | setup_test_locale(); 139 | CHECK(!from_string("#ZYXUIOPQ")); 140 | setup_test_locale(); 141 | CHECK(!from_string("#AUIO")); // invalid color 142 | setup_test_locale(); 143 | CHECK(!from_string("1.0 0.5 0.5 1.0 1.0")); // invalid color 144 | setup_test_locale(); 145 | CHECK(!from_string("1.0 0.5 0.5 1.0 asdf")); // invalid color 146 | setup_test_locale(); 147 | CHECK(!from_string("1.0 0.5")); // invalid color 148 | 149 | setup_test_locale(); 150 | CHECK(to_string(color_t{0, 0, 0, 0}) == "#00000000"); 151 | setup_test_locale(); 152 | CHECK(to_string(color_t{0.4, 0.8, 0.3686274, 153 | 0.9686274}) == "#66CC5EF7"); 154 | setup_test_locale(); 155 | CHECK(to_string(color_t{1, 1, 1, 1}) == "#FFFFFFFF"); 156 | } 157 | -------------------------------------------------------------------------------- /include/wayfire/config/option-wrapper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace wf 9 | { 10 | template 11 | using option_sptr_t = std::shared_ptr>; 12 | 13 | /** 14 | * Create an option which has a static value. 15 | */ 16 | template 17 | option_sptr_t create_option(T value) 18 | { 19 | return std::make_shared>("Static", value); 20 | } 21 | 22 | /** 23 | * Create an option which has a static value, 24 | * from the given string description. 25 | */ 26 | template 27 | option_sptr_t create_option_string(const std::string& value) 28 | { 29 | return std::make_shared>("Static", 30 | wf::option_type::from_string(value).value()); 31 | } 32 | 33 | /** 34 | * A helper to check whether the given type is a specialization of std::vector 35 | */ 36 | template 37 | struct is_std_vector : public std::false_type {}; 38 | 39 | template 40 | struct is_std_vector> : public std::true_type {}; 41 | 42 | template 43 | void get_value_from_compound_option( 44 | config::compound_option_t *opt, config::compound_list_t& list) 45 | { 46 | list = opt->get_value(); 47 | } 48 | 49 | /** 50 | * A simple wrapper around a config option. 51 | * 52 | * This is a base class. Each application which uses it needs to subclass it 53 | * and override the load_raw_option() method. 54 | */ 55 | template 56 | class base_option_wrapper_t 57 | { 58 | public: 59 | using OptionType = std::conditional_t< 60 | is_std_vector::value, 61 | config::compound_option_t, 62 | config::option_t>; 63 | 64 | public: 65 | base_option_wrapper_t(const base_option_wrapper_t& other) = delete; 66 | base_option_wrapper_t& operator =( 67 | const base_option_wrapper_t& other) = delete; 68 | 69 | base_option_wrapper_t(base_option_wrapper_t&& other) = delete; 70 | base_option_wrapper_t& operator =( 71 | base_option_wrapper_t&& other) = delete; 72 | 73 | /** 74 | * Load the option with the given name from the config. 75 | * @throws logic_error if the option wrapper already has an option loaded. 76 | * @throws runtime_error if the given option does not exist or does not 77 | * match the type of the option wrapper. 78 | */ 79 | void load_option(const std::string& name) 80 | { 81 | _load_option(load_raw_option(name), name); 82 | } 83 | 84 | void load_option(std::shared_ptr section, const std::string& name) 85 | { 86 | _load_option(section->get_option_or(name), section->get_name() + "/" + name); 87 | } 88 | 89 | virtual ~base_option_wrapper_t() 90 | { 91 | if (raw_option) 92 | { 93 | raw_option->rem_updated_handler(&option_update_listener); 94 | } 95 | } 96 | 97 | /** Implicitly convertible to the value of the option */ 98 | operator decltype(auto)() const WF_LIFETIMEBOUND 99 | { 100 | return this->value(); 101 | } 102 | 103 | decltype(auto) value() const WF_LIFETIMEBOUND 104 | { 105 | if constexpr (is_std_vector::value) 106 | { 107 | Type list; 108 | get_value_from_compound_option(this->raw_option.get(), list); 109 | return list; 110 | } else 111 | { 112 | return raw_option->get_value(); 113 | } 114 | } 115 | 116 | operator std::shared_ptr() const 117 | { 118 | return raw_option; 119 | } 120 | 121 | /** Set a callback to execute when the option value changes. */ 122 | void set_callback(std::function callback) 123 | { 124 | this->on_update = callback; 125 | } 126 | 127 | protected: 128 | std::function on_update; 129 | wf::config::option_base_t::updated_callback_t option_update_listener; 130 | 131 | /** The actual option wrapped by the option wrapper */ 132 | std::shared_ptr raw_option; 133 | 134 | /** 135 | * Initialize the option wrapper. 136 | */ 137 | base_option_wrapper_t() 138 | { 139 | option_update_listener = [=] () 140 | { 141 | if (this->on_update) 142 | { 143 | this->on_update(); 144 | } 145 | }; 146 | } 147 | 148 | /** 149 | * Load the option with the given name from the application configuration. 150 | */ 151 | virtual std::shared_ptr load_raw_option( 152 | const std::string& name) = 0; 153 | 154 | void _load_option(std::shared_ptr _option, const std::string& name) 155 | { 156 | if (raw_option) 157 | { 158 | throw std::logic_error( 159 | "Loading an option into option wrapper twice!"); 160 | } 161 | 162 | if (_option == nullptr) 163 | { 164 | throw std::runtime_error("No such option: " + std::string(name)); 165 | } 166 | 167 | raw_option = std::dynamic_pointer_cast(_option); 168 | if (raw_option == nullptr) 169 | { 170 | throw std::runtime_error("Bad option type: " + name); 171 | } 172 | 173 | raw_option->add_updated_handler(&option_update_listener); 174 | } 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /test/duration_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace wf; 9 | using namespace wf::config; 10 | using namespace wf::animation; 11 | 12 | TEST_CASE("wf::animation::duration_t") 13 | { 14 | duration_t duration; 15 | 16 | auto check_lifetime = [&] () 17 | { 18 | CHECK(duration.running() == false); 19 | CHECK(duration.progress() == doctest::Approx{1.0}); 20 | 21 | duration.start(); 22 | CHECK(duration.running()); 23 | CHECK(duration.progress() == doctest::Approx{0.0}); 24 | 25 | usleep(50000); 26 | CHECK(duration.progress() == doctest::Approx{0.5}.epsilon(0.1)); 27 | CHECK(duration.running()); 28 | CHECK(duration.running()); 29 | usleep(60000); 30 | 31 | /* At this point, duration must be finished */ 32 | CHECK(duration.progress() == doctest::Approx{1.0}.epsilon(0.01)); 33 | CHECK(duration.running()); // one last time 34 | CHECK(duration.running() == false); 35 | CHECK(duration.running() == false); 36 | }; 37 | 38 | SUBCASE("Int option") 39 | { 40 | auto length = std::make_shared>("length", 100); 41 | duration = duration_t{length, smoothing::linear}; 42 | } 43 | 44 | SUBCASE("Animation option") 45 | { 46 | auto length = std::make_shared>("length", 47 | wf::animation_description_t{ 48 | .length_ms = 100, 49 | .easing = smoothing::linear, 50 | .easing_name = "linear", 51 | }); 52 | duration = duration_t{length}; 53 | } 54 | 55 | /* Check twice, so that we can test restarting */ 56 | check_lifetime(); 57 | check_lifetime(); 58 | 59 | auto check_reverse_duration = [&] () 60 | { 61 | auto direction = duration.get_direction(); 62 | CHECK(duration.running() == false); 63 | CHECK(duration.progress() == doctest::Approx{direction ? 1.0 : 0.0}); 64 | 65 | duration.start(); 66 | CHECK(duration.running()); 67 | CHECK(duration.progress() == doctest::Approx{direction ? 0.0 : 1.0}); 68 | 69 | usleep(75000); 70 | CHECK(duration.progress() == doctest::Approx{direction ? 0.75 : 0.25}.epsilon( 71 | 0.1)); 72 | duration.reverse(); 73 | usleep(50000); 74 | CHECK(duration.progress() == doctest::Approx{direction ? 0.25 : 0.75}.epsilon( 75 | 0.1)); 76 | CHECK(duration.running()); 77 | CHECK(duration.running()); 78 | usleep(35000); 79 | 80 | /* At this point, duration must be finished */ 81 | CHECK(duration.progress() == 82 | doctest::Approx{direction ? 0.0 : 1.0}.epsilon(0.01)); 83 | CHECK(duration.running()); // one last time 84 | CHECK(duration.running() == false); 85 | CHECK(duration.running() == false); 86 | }; 87 | 88 | /* Check twice, so that we can test direction */ 89 | check_reverse_duration(); 90 | check_reverse_duration(); 91 | } 92 | 93 | TEST_CASE("wf::animation::timed_transition_t") 94 | { 95 | const double start = 1.0; 96 | const double end = 2.0; 97 | const double overend = 3.0; 98 | const double middle = (start + end) / 2.0; 99 | 100 | auto length = std::make_shared>("length", 100); 101 | duration_t duration{length, smoothing::linear}; 102 | timed_transition_t transition{duration}; 103 | timed_transition_t transition2{duration, start, end}; 104 | transition.set(start, end); 105 | CHECK(transition.start == doctest::Approx(start)); 106 | CHECK(transition.end == doctest::Approx(end)); 107 | 108 | duration.start(); 109 | CHECK((double)transition == doctest::Approx(start)); 110 | usleep(50000); 111 | CHECK((double)transition == doctest::Approx(middle).epsilon(0.1)); 112 | CHECK((double)transition2 == doctest::Approx(middle).epsilon(0.1)); 113 | transition.restart_with_end(overend); 114 | transition2.restart_same_end(); 115 | CHECK(transition.start == doctest::Approx(middle).epsilon(0.1)); 116 | CHECK(transition2.start == doctest::Approx(middle).epsilon(0.1)); 117 | CHECK(transition.end == doctest::Approx(overend)); 118 | CHECK(transition2.end == doctest::Approx(end)); 119 | usleep(60000); 120 | CHECK((double)transition == doctest::Approx(overend).epsilon(0.1)); 121 | 122 | transition.flip(); 123 | CHECK(transition.start == doctest::Approx(3.0).epsilon(0.1)); 124 | CHECK(transition.end == doctest::Approx(middle).epsilon(0.1)); 125 | } 126 | 127 | TEST_CASE("wf::animation::simple_animation_t") 128 | { 129 | auto length = std::make_shared>("length", 10); 130 | simple_animation_t anim{length, smoothing::linear}; 131 | 132 | auto cycle_through = [&] (double s, double e, bool x1, bool x2) 133 | { 134 | if (!x1 && !x2) 135 | { 136 | anim.animate(s, e); 137 | } else if (!x2) 138 | { 139 | anim.animate(e); 140 | } else if (!x1) 141 | { 142 | anim.animate(); 143 | } 144 | 145 | CHECK(anim.running()); 146 | CHECK((double)anim == doctest::Approx(s)); 147 | usleep(5000); 148 | CHECK((double)anim == doctest::Approx((s + e) / 2).epsilon(0.1)); 149 | CHECK(anim.running()); 150 | usleep(5500); 151 | CHECK((double)anim == doctest::Approx(e)); 152 | CHECK(anim.running()); 153 | CHECK(!anim.running()); 154 | }; 155 | 156 | cycle_through(1, 2, false, false); 157 | cycle_through(2, 3, true, false); 158 | cycle_through(3, 3, false, true); 159 | 160 | simple_animation_t sa; 161 | sa = simple_animation_t{length}; 162 | sa.animate(1, 2); 163 | CHECK((double)sa == doctest::Approx(1.0)); 164 | } 165 | -------------------------------------------------------------------------------- /src/compound-option.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "option-impl.hpp" 4 | 5 | using namespace wf::config; 6 | 7 | static bool begins_with(const std::string& a, const std::string& b) 8 | { 9 | return a.substr(0, b.size()) == b; 10 | } 11 | 12 | compound_option_t::compound_option_t(const std::string& name, 13 | entries_t&& entries, std::string type_hint) : option_base_t(name), 14 | list_type_hint( 15 | type_hint) 16 | { 17 | this->entries = std::move(entries); 18 | } 19 | 20 | void wf::config::update_compound_from_section( 21 | compound_option_t& compound, 22 | const std::shared_ptr& section) 23 | { 24 | const auto& options = section->get_registered_options(); 25 | 26 | const auto& should_ignore_option = [] (const std::shared_ptr& opt) 27 | { 28 | return xml::get_option_xml_node(opt) || !opt->priv->option_in_config_file; 29 | }; 30 | 31 | const auto& entries = compound.get_entries(); 32 | 33 | std::map> new_values; 34 | 35 | // find possible suffixes 36 | for (const auto& opt : options) 37 | { 38 | if (should_ignore_option(opt)) 39 | { 40 | continue; 41 | } 42 | 43 | // iterating from the end ta handle cases where there is a prefix which is a prefix of another prefix 44 | // for instance, if there are entries with prefixes `prefix_` and `prefix_smth_` (in that order), 45 | // then option with name `prefix_smth_suffix` will be recognised with prefix `prefix_smth_`. 46 | for (auto it = entries.rbegin(); it != entries.rend(); ++it) 47 | { 48 | const auto& entry = *it; 49 | if (begins_with(opt->get_name(), entry->get_prefix())) 50 | { 51 | new_values.emplace(opt->get_name().substr(entry->get_prefix().size()), entries.size() + 1); 52 | break; 53 | } 54 | } 55 | } 56 | 57 | compound_option_t::stored_type_t stored_value; 58 | for (auto& [suffix, value] : new_values) 59 | { 60 | value[0] = suffix; 61 | for (size_t i = 0; i < entries.size(); ++i) 62 | { 63 | if (const auto & entry_option = section->get_option_or(entries[i]->get_prefix() + suffix); 64 | entry_option && !should_ignore_option(entry_option)) 65 | { 66 | entry_option->priv->could_be_compound = true; 67 | if (entries[i]->is_parsable(entry_option->get_value_str())) 68 | { 69 | value[i + 1] = entry_option->get_value_str(); 70 | continue; 71 | } else 72 | { 73 | LOGE("Failed parsing option ", 74 | section->get_name() + "/" + entry_option->get_name(), 75 | " as part of the list option ", 76 | section->get_name() + "/" + compound.get_name(), 77 | ". Trying to use the default value."); 78 | } 79 | } 80 | 81 | if (const auto& default_value = entries[i]->get_default_value()) 82 | { 83 | value[i + 1] = *default_value; 84 | } else 85 | { 86 | value.clear(); 87 | break; 88 | } 89 | } 90 | 91 | if (!value.empty()) 92 | { 93 | stored_value.push_back(std::move(value)); 94 | for (size_t i = 0; i < entries.size(); ++i) 95 | { 96 | if (auto entry_option = section->get_option_or(entries[i]->get_prefix() + suffix)) 97 | { 98 | // The option was used as part of the compound option, do not issue warning for it! 99 | entry_option->priv->is_part_compound = true; 100 | } 101 | } 102 | } 103 | } 104 | 105 | compound.set_value_untyped(stored_value); 106 | } 107 | 108 | compound_option_t::stored_type_t compound_option_t::get_value_untyped() 109 | { 110 | return this->value; 111 | } 112 | 113 | bool compound_option_t::set_value_untyped(stored_type_t value) 114 | { 115 | for (auto& e : value) 116 | { 117 | if (e.size() != this->entries.size() + 1) 118 | { 119 | return false; 120 | } 121 | 122 | for (size_t i = 1; i <= this->entries.size(); i++) 123 | { 124 | if (!entries[i - 1]->is_parsable(e[i])) 125 | { 126 | return false; 127 | } 128 | } 129 | } 130 | 131 | this->value = value; 132 | notify_updated(); 133 | return true; 134 | } 135 | 136 | const compound_option_t::entries_t& compound_option_t::get_entries() const 137 | { 138 | return this->entries; 139 | } 140 | 141 | /* --------------------------- option_base_t impl --------------------------- */ 142 | std::shared_ptr compound_option_t::clone_option() const 143 | { 144 | entries_t cloned; 145 | for (auto& e : this->entries) 146 | { 147 | cloned.push_back( 148 | std::unique_ptr(e->clone())); 149 | } 150 | 151 | auto result = std::make_shared(get_name(), std::move(cloned)); 152 | result->value = this->value; 153 | return result; 154 | } 155 | 156 | bool wf::config::compound_option_t::set_value_str(const std::string&) 157 | { 158 | // XXX: not supported yet 159 | return false; 160 | } 161 | 162 | void wf::config::compound_option_t::reset_to_default() 163 | { 164 | this->value.clear(); 165 | } 166 | 167 | bool wf::config::compound_option_t::set_default_value_str(const std::string&) 168 | { 169 | // XXX: not supported yet 170 | return false; 171 | } 172 | 173 | std::string wf::config::compound_option_t::get_value_str() const 174 | { 175 | // XXX: not supported yet 176 | return ""; 177 | } 178 | 179 | std::string wf::config::compound_option_t::get_default_value_str() const 180 | { 181 | // XXX: not supported yet 182 | return ""; 183 | } 184 | -------------------------------------------------------------------------------- /include/wayfire/util/duration.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace wf 8 | { 9 | namespace animation 10 | { 11 | namespace smoothing 12 | { 13 | /** 14 | * A smooth function is a function which takes a double in [0, 1] and returns another double in R. Both ranges 15 | * represent percentage of a progress of an animation. 16 | */ 17 | using smooth_function = std::function; 18 | 19 | /** linear smoothing function, i.e x -> x */ 20 | extern smooth_function linear; 21 | /** "circle" smoothing function, i.e x -> sqrt(2x - x*x) */ 22 | extern smooth_function circle; 23 | /** "sigmoid" smoothing function, i.e x -> 1.0 / (1 + exp(-12 * x + 6)) */ 24 | extern smooth_function sigmoid; 25 | /** custom cubic-bezier as in CSS */ 26 | extern smooth_function get_cubic_bezier(double x1, double y1, double x2, double y2); 27 | 28 | std::vector get_available_smooth_functions(); 29 | } 30 | } 31 | 32 | struct animation_description_t 33 | { 34 | int length_ms; 35 | animation::smoothing::smooth_function easing; 36 | std::string easing_name; 37 | 38 | bool operator ==(const animation_description_t& other) const; 39 | }; 40 | 41 | namespace option_type 42 | { 43 | /** 44 | * Parse the string as an animation description. 45 | */ 46 | template<> 47 | std::optional from_string(const std::string& value); 48 | 49 | /** 50 | * Convert the given animation description to a string. 51 | */ 52 | template<> 53 | std::string to_string(const animation_description_t& value); 54 | } 55 | 56 | namespace animation 57 | { 58 | /** 59 | * A transition from start to end. 60 | */ 61 | struct transition_t 62 | { 63 | double start, end; 64 | }; 65 | 66 | /** 67 | * duration_t is a class which can be used to track progress over a specific time interval. 68 | */ 69 | class duration_t 70 | { 71 | public: 72 | /** 73 | * Construct a new duration. Initially, the duration is not running and its progress is 1. 74 | * 75 | * @param length The length of the duration in milliseconds. 76 | * @param smooth The smoothing function for transitions. 77 | */ 78 | duration_t(std::shared_ptr> length = nullptr, 79 | smoothing::smooth_function smooth = smoothing::circle); 80 | 81 | duration_t(std::shared_ptr> length); 82 | 83 | /* Copy-constructor */ 84 | duration_t(const duration_t& other); 85 | /* Copy-assignment */ 86 | duration_t& operator =(const duration_t& other); 87 | 88 | /* Move-constructor */ 89 | duration_t(duration_t&& other) = default; 90 | /* Move-assignment */ 91 | duration_t& operator =(duration_t&& other) = default; 92 | 93 | /** 94 | * Start the duration. This means that the progress will get reset to 0. 95 | */ 96 | void start(); 97 | 98 | /** 99 | * Get the progress of the duration in percentage. The progress will be smoothed using the smoothing 100 | * function. 101 | * 102 | * @return The current progress after smoothing. It is guaranteed that when the duration starts, progress 103 | * will be close to 0, and when it is finished, it will be close to 1. 104 | */ 105 | double progress() const; 106 | 107 | /** 108 | * Check if the duration is still running. Note that even when the duration first finishes, this function 109 | * will still return that the function is running one time. 110 | * 111 | * @return Whether the duration still has not elapsed. 112 | */ 113 | bool running(); 114 | 115 | /** 116 | * Reverse the duration. The progress will remain the same but the direction will reverse toward the 117 | * opposite start or end point. 118 | */ 119 | void reverse(); 120 | 121 | /** 122 | * Get duration direction. 123 | * 0: reverse 1: forward 124 | */ 125 | int get_direction(); 126 | 127 | class impl; 128 | /** Implementation details. */ 129 | std::shared_ptr priv; 130 | }; 131 | 132 | /** 133 | * A timed transition is a transition between two states which happens over a period of time. 134 | * 135 | * During the transition, the current state is smoothly interpolated between start and end. 136 | */ 137 | struct timed_transition_t : public transition_t 138 | { 139 | /** 140 | * Construct a new timed transition using the given duration to measure progress. 141 | * 142 | * @duration The duration to use for time measurement 143 | * @start The start state. 144 | * @end The end state. 145 | */ 146 | timed_transition_t(const duration_t& duration, 147 | double start = 0, double end = 0); 148 | 149 | /** 150 | * Set the transition start to the current state and the end to the given 151 | * @new_end. 152 | */ 153 | void restart_with_end(double new_end); 154 | 155 | /** 156 | * Set the transition start to the current state, and don't change the end. 157 | */ 158 | void restart_same_end(); 159 | 160 | /** 161 | * Set the transition start and end state. 162 | * @param start The start of the transition. 163 | * @param end The end of the transition. 164 | */ 165 | void set(double start, double end); 166 | 167 | /** 168 | * Swap start and end values. 169 | */ 170 | void flip(); 171 | 172 | /** 173 | * Implicitly convert the transition to its current state. 174 | */ 175 | operator double() const; 176 | 177 | private: 178 | std::shared_ptr duration; 179 | }; 180 | 181 | class simple_animation_t : public duration_t, public timed_transition_t 182 | { 183 | public: 184 | simple_animation_t( 185 | std::shared_ptr> length = nullptr, 186 | smoothing::smooth_function smooth = smoothing::circle); 187 | 188 | simple_animation_t(std::shared_ptr> length); 189 | 190 | /** 191 | * Set the start and the end of the animation and start the duration. 192 | */ 193 | void animate(double start, double end); 194 | 195 | /** 196 | * Animate from the current progress to the given end, and start the duration. 197 | */ 198 | void animate(double end); 199 | 200 | /** 201 | * Animate from the current progress to the current end, and start the duration. 202 | */ 203 | void animate(); 204 | }; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /include/wayfire/config/option.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #ifdef __clang__ 10 | #define WF_LIFETIMEBOUND [[clang::lifetimebound]] 11 | #else 12 | #define WF_LIFETIMEBOUND 13 | #endif 14 | 15 | namespace wf 16 | { 17 | namespace config 18 | { 19 | /** 20 | * A base class for all option types. 21 | */ 22 | class option_base_t 23 | { 24 | public: 25 | virtual ~option_base_t(); 26 | option_base_t(const option_base_t& other) = delete; 27 | option_base_t& operator =(const option_base_t& other) = delete; 28 | 29 | /** @return The name of the option */ 30 | const std::string& get_name() const WF_LIFETIMEBOUND; 31 | 32 | /** @return A copy of the option */ 33 | virtual std::shared_ptr clone_option() const = 0; 34 | 35 | /** 36 | * Set the option value from the given string. 37 | * Invalid values are ignored. 38 | * 39 | * @return true if the option value was updated. 40 | */ 41 | virtual bool set_value_str(const std::string& value) = 0; 42 | 43 | /** Reset the option to its default value. */ 44 | virtual void reset_to_default() = 0; 45 | 46 | /** 47 | * Change the default value of an option. Note that this will not change the 48 | * option value, only its default value. 49 | * 50 | * If the new default value is invalid, the request will be ignored. 51 | * @return true if the default value was updated. 52 | */ 53 | virtual bool set_default_value_str(const std::string& default_value) = 0; 54 | 55 | /** Get the option value in string format */ 56 | virtual std::string get_value_str() const = 0; 57 | 58 | /** Get the default option value in string format */ 59 | virtual std::string get_default_value_str() const = 0; 60 | 61 | /** 62 | * A function to be executed when the option value changes. 63 | */ 64 | using updated_callback_t = std::function; 65 | 66 | /** 67 | * Register a new callback to execute when the option value changes. 68 | */ 69 | void add_updated_handler(updated_callback_t *callback); 70 | 71 | /** 72 | * Unregister a callback to execute when the option value changes. 73 | * If the same callback has been registered multiple times, this unregister 74 | * all registered instances. 75 | */ 76 | void rem_updated_handler(updated_callback_t *callback); 77 | 78 | /** 79 | * Set the lock status of an option, this is reference-counted. 80 | * 81 | * An option is unlocked by default. When an option is locked, the option 82 | * should not be modified by any config backend (for ex. when reading from 83 | * a file). 84 | * 85 | * Note that changing the value of the option manually still works. 86 | */ 87 | void set_locked(bool locked = true); 88 | 89 | /** 90 | * Get the current locked status. 91 | */ 92 | bool is_locked() const; 93 | 94 | struct impl; 95 | std::unique_ptr priv; 96 | 97 | protected: 98 | /** Construct a new option with the given name. */ 99 | option_base_t(const std::string& name); 100 | 101 | /** Notify all watchers */ 102 | void notify_updated() const; 103 | 104 | /** Initialize a cloned version of this option. */ 105 | void init_clone(option_base_t& clone) const; 106 | }; 107 | 108 | /** 109 | * A base class for options which can have minimum and maximum. 110 | * By default, no bounding checks are enabled. 111 | */ 112 | template 113 | class bounded_option_base_t 114 | { 115 | protected: 116 | Type closest_valid_value(const Type& value) const 117 | { 118 | return value; 119 | } 120 | }; 121 | 122 | /** 123 | * Specialization for option types which do support bounded values. 124 | */ 125 | template 126 | class bounded_option_base_t 127 | { 128 | public: 129 | /** @return The minimal permissible value for this option, if it is set. */ 130 | std::optional get_minimum() const 131 | { 132 | return minimum; 133 | } 134 | 135 | /** @return The maximal permissible value for this option, if it is set. */ 136 | std::optional get_maximum() const 137 | { 138 | return maximum; 139 | } 140 | 141 | protected: 142 | std::optional minimum; 143 | std::optional maximum; 144 | 145 | /** 146 | * @return The closest possible value 147 | */ 148 | Type closest_valid_value(const Type& value) const 149 | { 150 | auto real_minimum = 151 | minimum.value_or(std::numeric_limits::lowest()); 152 | auto real_maximum = 153 | maximum.value_or(std::numeric_limits::max()); 154 | 155 | if (value < real_minimum) 156 | { 157 | return real_minimum; 158 | } 159 | 160 | if (value > real_maximum) 161 | { 162 | return real_maximum; 163 | } 164 | 165 | return value; 166 | } 167 | }; 168 | 169 | namespace detail 170 | { 171 | template using boundable_type_only = 172 | std::enable_if_t::value, Result>; 173 | } 174 | 175 | /** 176 | * Represents an option of the given type. 177 | */ 178 | template 179 | class option_t : public option_base_t, 180 | public bounded_option_base_t::value> 181 | { 182 | public: 183 | /** 184 | * Create a new option with the given name and default value. 185 | */ 186 | option_t(const std::string& name, Type def_value) : 187 | option_base_t(name), default_value(def_value), value(default_value) 188 | {} 189 | 190 | /** 191 | * Create a copy of the option. 192 | */ 193 | virtual std::shared_ptr clone_option() const override 194 | { 195 | auto result = std::make_shared(get_name(), get_default_value()); 196 | result->set_value(get_value()); 197 | if constexpr (std::is_arithmetic::value) 198 | { 199 | result->minimum = this->minimum; 200 | result->maximum = this->maximum; 201 | } 202 | 203 | init_clone(*result); 204 | return result; 205 | } 206 | 207 | /** 208 | * Set the value of the option from the given string. 209 | * The value will be auto-clamped to the defined bounds, if they exist. 210 | * If the value actually changes, the updated handlers will be called. 211 | */ 212 | virtual bool set_value_str(const std::string& new_value_str) override 213 | { 214 | auto new_value = option_type::from_string(new_value_str); 215 | if (new_value) 216 | { 217 | set_value(new_value.value()); 218 | return true; 219 | } 220 | 221 | return false; 222 | } 223 | 224 | /** 225 | * Reset the option to its default value. 226 | */ 227 | virtual void reset_to_default() override 228 | { 229 | set_value(default_value); 230 | } 231 | 232 | /** 233 | * Change the default value of the function, if possible. 234 | */ 235 | virtual bool set_default_value_str(const std::string& defvalue) override 236 | { 237 | auto parsed = option_type::from_string(defvalue); 238 | if (parsed) 239 | { 240 | this->default_value = parsed.value(); 241 | return true; 242 | } 243 | 244 | return false; 245 | } 246 | 247 | /** 248 | * Set the value of the option. 249 | * The value will be auto-clamped to the defined bounds, if they exist. 250 | * If the value actually changes, the updated handlers will be called. 251 | */ 252 | void set_value(const Type& new_value) 253 | { 254 | auto real_value = this->closest_valid_value(new_value); 255 | if (!(this->value == real_value)) 256 | { 257 | this->value = real_value; 258 | this->notify_updated(); 259 | } 260 | } 261 | 262 | const Type& get_value() const WF_LIFETIMEBOUND 263 | { 264 | return value; 265 | } 266 | 267 | const Type& get_default_value() const WF_LIFETIMEBOUND 268 | { 269 | return default_value; 270 | } 271 | 272 | virtual std::string get_value_str() const override 273 | { 274 | return option_type::to_string(get_value()); 275 | } 276 | 277 | virtual std::string get_default_value_str() const override 278 | { 279 | return option_type::to_string(get_default_value()); 280 | } 281 | 282 | public: 283 | /** 284 | * Set the minimum permissible value for arithmetic type options. 285 | * An attempt to set the value to a value below the minimum will set the 286 | * value of the option to the minimum. 287 | */ 288 | template 289 | detail::boundable_type_only set_minimum(Type min) 290 | { 291 | this->minimum = {min}; 292 | this->value = this->closest_valid_value(this->value); 293 | } 294 | 295 | /** 296 | * Set the maximum permissible value for arithmetic type options. 297 | * An attempt to set the value to a value above the maximum will set the 298 | * value of the option to the maximum. 299 | */ 300 | template 301 | detail::boundable_type_only set_maximum(Type max) 302 | { 303 | this->maximum = {max}; 304 | this->value = this->closest_valid_value(this->value); 305 | } 306 | 307 | protected: 308 | Type default_value; /* default value */ 309 | Type value; /* current value */ 310 | }; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /include/wayfire/config/compound-option.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace wf 14 | { 15 | namespace config 16 | { 17 | template 18 | using compound_list_t = 19 | std::vector>; 20 | 21 | template 22 | using simple_list_t = 23 | std::vector>; 24 | 25 | template 26 | std::tuple pop_first(const std::tuple& tuple) 27 | { 28 | return std::apply([] (auto&&, const auto&... args) 29 | { 30 | return std::tie(args...); 31 | }, tuple); 32 | } 33 | 34 | template 35 | std::tuple push_first(const T1& t, const std::tuple& tuple) 36 | { 37 | return std::tuple_cat(std::tie(t), tuple); 38 | } 39 | 40 | /** 41 | * A base class containing information about an entry in a tuple. 42 | */ 43 | class compound_option_entry_base_t 44 | { 45 | public: 46 | 47 | virtual ~compound_option_entry_base_t() = default; 48 | 49 | /** @return The prefix of the tuple entry. */ 50 | const std::string& get_prefix() const WF_LIFETIMEBOUND 51 | { 52 | return prefix; 53 | } 54 | 55 | /** @return The name of the tuple entry. */ 56 | const std::string& get_name() const WF_LIFETIMEBOUND 57 | { 58 | return name; 59 | } 60 | 61 | /** @return The untyped default value of the tuple entry. */ 62 | const std::optional& get_default_value() const WF_LIFETIMEBOUND 63 | { 64 | return default_value; 65 | } 66 | 67 | /** 68 | * Try to parse the given value. 69 | * 70 | * @param update Whether to override the stored value. 71 | */ 72 | virtual bool is_parsable(const std::string&) const = 0; 73 | 74 | /** Clone this entry */ 75 | virtual compound_option_entry_base_t *clone() const = 0; 76 | 77 | protected: 78 | compound_option_entry_base_t() = default; 79 | std::string prefix; 80 | std::string name; 81 | std::optional default_value; 82 | }; 83 | 84 | template 85 | class compound_option_entry_t : public compound_option_entry_base_t 86 | { 87 | public: 88 | compound_option_entry_t(const std::string& prefix, const std::string& name = "", 89 | const std::optional& default_value = std::nullopt) 90 | { 91 | this->prefix = prefix; 92 | this->name = name; 93 | this->default_value = default_value; 94 | assert(!this->default_value || is_parsable(this->default_value.value())); 95 | } 96 | 97 | compound_option_entry_base_t *clone() const override 98 | { 99 | return new compound_option_entry_t(*this); 100 | } 101 | 102 | /** 103 | * Try to parse the given value. 104 | * 105 | * @param update Whether to override the stored value. 106 | */ 107 | bool is_parsable(const std::string& str) const override 108 | { 109 | return option_type::from_string(str).has_value(); 110 | } 111 | }; 112 | 113 | /** 114 | * Compound options are a special class of options which can hold multiple 115 | * string-tagged tuples. They are constructed from multiple untyped options 116 | * in the config file. 117 | */ 118 | 119 | class compound_option_t : public option_base_t 120 | { 121 | public: 122 | using entries_t = std::vector>; 123 | /** 124 | * Construct a new compound option, with the types given in the template 125 | * arguments. 126 | * 127 | * @param name The name of the option. 128 | * @param prefixes The prefixes used for grouping in the config file. 129 | * Example: Consider a compound option with type and two 130 | * prefixes {"prefix1_", "prefix2_"}. In the config file, the options are: 131 | * 132 | * prefix1_key1 = v11 133 | * prefix2_key1 = v21 134 | * prefix1_key2 = v12 135 | * prefix2_key2 = v22 136 | * 137 | * Options are grouped by suffixes (key1 and key2), and the tuples then 138 | * are formed by taking the values of the options with each prefix. 139 | * So, the tuples contained in the compound option in the end are: 140 | * 141 | * (key1, v11, v21) 142 | * (key2, v12, v22) 143 | * @param type_hint What type of dynamic-list is this option. This stores 144 | * the type-hint attribute of the option specified in the xml. Currently, 145 | * the valid types are plain, dict and tuple (default). Type-hint is 146 | * mainly used as hint to save the config option in a "pretty" way. 147 | * Config formats are free to ignore this type hint. 148 | */ 149 | compound_option_t(const std::string& name, entries_t&& entries, 150 | std::string type_hint = "tuple"); 151 | 152 | /** 153 | * Parse the compound option with the given types. 154 | * 155 | * Throws an exception in case of wrong template types. 156 | */ 157 | template 158 | compound_list_t get_value() const 159 | { 160 | compound_list_t result; 161 | result.resize(value.size()); 162 | build_recursive<0, Args...>(result); 163 | return result; 164 | } 165 | 166 | template 167 | simple_list_t get_value_simple() const 168 | { 169 | auto list = get_value(); 170 | simple_list_t result; 171 | for (const auto& val : list) 172 | { 173 | result.push_back(pop_first(val)); 174 | } 175 | 176 | return result; 177 | } 178 | 179 | /** 180 | * Set the value of the option. 181 | * 182 | * Throws an exception in case of wrong template types. 183 | */ 184 | template 185 | void set_value(const compound_list_t& value) 186 | { 187 | assert(sizeof...(Args) == this->entries.size()); 188 | this->value.assign(value.size(), {}); 189 | push_recursive<0>(value); 190 | notify_updated(); 191 | } 192 | 193 | /** 194 | * Set the value of the option. 195 | * 196 | * Throws an exception in case of wrong template types. 197 | */ 198 | template 199 | void set_value_simple(const simple_list_t& value) 200 | { 201 | compound_list_t list; 202 | for (int i = 0; i < (int)value.size(); i++) 203 | { 204 | list.push_back(push_first(std::to_string(i), value[i])); 205 | } 206 | 207 | this->set_value(list); 208 | } 209 | 210 | using stored_type_t = std::vector>; 211 | /** 212 | * Get the string data stored in the compound option. 213 | */ 214 | stored_type_t get_value_untyped(); 215 | 216 | /** 217 | * Set the data contained in the option, from a vector containing 218 | * strings which describe the individual elements. 219 | * 220 | * @return True if the operation was successful. 221 | */ 222 | bool set_value_untyped(stored_type_t value); 223 | 224 | /** 225 | * Get the type information about entries in the option. 226 | */ 227 | const entries_t& get_entries() const WF_LIFETIMEBOUND; 228 | 229 | /** 230 | * Check if this compound option has named tuples. 231 | */ 232 | const std::string& get_type_hint() const WF_LIFETIMEBOUND 233 | { 234 | return list_type_hint; 235 | } 236 | 237 | private: 238 | /** 239 | * Current value stored in the option. 240 | * The first element is the name of the tuple, followed by the string values 241 | * of each element. 242 | */ 243 | stored_type_t value; 244 | 245 | /** Entry types with which the option was created. */ 246 | entries_t entries; 247 | 248 | /** What type of dynamic-list is this: plain, dics, tuple */ 249 | std::string list_type_hint; 250 | 251 | /** 252 | * Set the n-th element in the result tuples by reading from the stored 253 | * values in this option. 254 | */ 255 | template 256 | void build_recursive(compound_list_t& result) const 257 | { 258 | for (size_t i = 0; i < result.size(); i++) 259 | { 260 | using type_t = typename std::tuple_element>::type; 262 | 263 | std::get(result[i]) = option_type::from_string( 264 | this->value[i][n]).value(); 265 | } 266 | 267 | // Recursively build the (N+1)'th entries 268 | if constexpr (n < sizeof...(Args)) 269 | { 270 | build_recursive(result); 271 | } 272 | } 273 | 274 | template 275 | void push_recursive(const compound_list_t& new_value) 276 | { 277 | for (size_t i = 0; i < new_value.size(); i++) 278 | { 279 | using type_t = typename std::tuple_element>::type; 281 | 282 | this->value[i].push_back(option_type::to_string( 283 | std::get(new_value[i]))); 284 | } 285 | 286 | // Recursively build the (N+1)'th entries 287 | if constexpr (n < sizeof...(Args)) 288 | { 289 | push_recursive(new_value); 290 | } 291 | } 292 | 293 | public: // Implementation of option_base_t 294 | std::shared_ptr clone_option() const override; 295 | bool set_value_str(const std::string&) override; 296 | void reset_to_default() override; 297 | bool set_default_value_str(const std::string&) override; 298 | std::string get_value_str() const override; 299 | std::string get_default_value_str() const override; 300 | }; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/duration.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | double bezier_helper(double t, double p0, double p1, double p2, double p3) 12 | { 13 | const double u = 1 - t; 14 | return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3; 15 | } 16 | 17 | inline bool epsilon_comparison(double a, double b) 18 | { 19 | return std::fabs(a - b) <= std::numeric_limits::epsilon() * std::fabs(a + b); 20 | } 21 | 22 | namespace wf 23 | { 24 | namespace animation 25 | { 26 | namespace smoothing 27 | { 28 | smooth_function linear = 29 | [] (double x) -> double { return x; }; 30 | smooth_function circle = 31 | [] (double x) -> double { return std::sqrt(2 * x - x * x); }; 32 | 33 | const double sigmoid_max = 1 + std::exp(-6); 34 | smooth_function sigmoid = 35 | [] (double x) -> double { return sigmoid_max / (1 + exp(-12 * x + 6)); }; 36 | 37 | smooth_function get_cubic_bezier(double x1, double y1, double x2, double y2) 38 | { 39 | // https://en.wikipedia.org/wiki/Newton%27s_method 40 | return [=] (double x) 41 | { 42 | double t = x; 43 | for (int i = 0; i < 10; ++i) 44 | { 45 | const double f = bezier_helper(t, 0, x1, x2, 1) - x; 46 | const double df = 3 * (1 - t) * (1 - t) * x1 + 6 * (1 - t) * t * (x2 - x1) + 3 * t * t * (1 - x2); 47 | if (std::abs(f) < 1e-6) 48 | { 49 | break; 50 | } 51 | 52 | t -= f / df; 53 | } 54 | 55 | return bezier_helper(t, 0, y1, y2, 1); 56 | }; 57 | } 58 | } 59 | } // namespace animation 60 | } 61 | 62 | bool wf::animation_description_t::operator ==(const animation_description_t & other) const 63 | { 64 | if (easing_name == other.easing_name) 65 | { 66 | return (length_ms == other.length_ms); 67 | } 68 | 69 | // Cubic-bezier easings need parsing to handle epsilon 70 | std::stringstream easing_a(easing_name); 71 | std::stringstream easing_b(easing_name); 72 | std::string easing_type_a, easing_type_b; 73 | easing_a >> easing_type_a; 74 | easing_b >> easing_type_b; 75 | if ((easing_type_a != "cubic-bezier") || (easing_type_b != "cubic-bezier")) 76 | { 77 | return false; 78 | } 79 | 80 | double x1_a, y1_a, x2_a, y2_a, x1_b, y1_b, x2_b, y2_b; 81 | easing_a >> x1_a >> y1_a >> x2_a >> y2_a; 82 | easing_b >> x1_b >> y1_b >> x2_b >> y2_b; 83 | return epsilon_comparison(x1_a, x1_b) && 84 | epsilon_comparison(y1_a, y1_b) && 85 | epsilon_comparison(x2_a, x2_b) && 86 | epsilon_comparison(y2_b, y2_b); 87 | } 88 | 89 | class wf::animation::duration_t::impl 90 | { 91 | public: 92 | decltype(std::chrono::system_clock::now()) start_point; 93 | 94 | std::shared_ptr> length; 95 | std::shared_ptr> descr; 96 | 97 | smoothing::smooth_function smooth_function; 98 | bool is_running = false; 99 | bool reverse = false; 100 | 101 | int64_t get_elapsed() const 102 | { 103 | using namespace std::chrono; 104 | auto now = system_clock::now(); 105 | return duration_cast(now - start_point).count(); 106 | } 107 | 108 | int get_duration() const 109 | { 110 | if (descr) 111 | { 112 | return std::max(1, descr->get_value().length_ms); 113 | } 114 | 115 | if (length) 116 | { 117 | return std::max(1, length->get_value()); 118 | } 119 | 120 | LOGD("Calling methods on wf::animation::duration_t without a length"); 121 | return 1; 122 | } 123 | 124 | bool is_ready() const 125 | { 126 | return get_elapsed() >= get_duration(); 127 | } 128 | 129 | double get_progress_percentage() const 130 | { 131 | if ((!length && !descr) || is_ready()) 132 | { 133 | return 1.0; 134 | } 135 | 136 | auto progress = 1.0 * get_elapsed() / get_duration(); 137 | if (reverse) 138 | { 139 | progress = 1.0 - progress; 140 | } 141 | 142 | return std::clamp(progress, 0.0, 1.0); 143 | } 144 | 145 | double progress() const 146 | { 147 | if (is_ready()) 148 | { 149 | return reverse ? 0.0 : 1.0; 150 | } 151 | 152 | if (descr) 153 | { 154 | return descr->get_value().easing(get_progress_percentage()); 155 | } else 156 | { 157 | return smooth_function(get_progress_percentage()); 158 | } 159 | } 160 | }; 161 | 162 | wf::animation::duration_t::duration_t( 163 | std::shared_ptr> length, 164 | smoothing::smooth_function smooth) 165 | { 166 | this->priv = std::make_shared(); 167 | this->priv->length = length; 168 | this->priv->smooth_function = smooth; 169 | } 170 | 171 | wf::animation::duration_t::duration_t( 172 | std::shared_ptr> length) 173 | { 174 | this->priv = std::make_shared(); 175 | this->priv->descr = length; 176 | } 177 | 178 | wf::animation::duration_t::duration_t(const duration_t& other) 179 | { 180 | this->priv = std::make_shared(*other.priv); 181 | } 182 | 183 | wf::animation::duration_t& wf::animation::duration_t::operator =( 184 | const duration_t& other) 185 | { 186 | if (&other != this) 187 | { 188 | this->priv = std::make_shared(*other.priv); 189 | } 190 | 191 | return *this; 192 | } 193 | 194 | void wf::animation::duration_t::start() 195 | { 196 | this->priv->is_running = 1; 197 | this->priv->start_point = std::chrono::system_clock::now(); 198 | } 199 | 200 | double wf::animation::duration_t::progress() const 201 | { 202 | return this->priv->progress(); 203 | } 204 | 205 | bool wf::animation::duration_t::running() 206 | { 207 | if (this->priv->is_ready()) 208 | { 209 | bool was_running = this->priv->is_running; 210 | this->priv->is_running = false; 211 | return was_running; 212 | } 213 | 214 | return true; 215 | } 216 | 217 | void wf::animation::duration_t::reverse() 218 | { 219 | auto total_duration = this->priv->get_duration(); 220 | auto elapsed = std::min(this->priv->get_elapsed(), (int64_t)total_duration); 221 | auto remaining = std::chrono::milliseconds(total_duration - elapsed); 222 | this->priv->start_point = std::chrono::system_clock::now() - remaining; 223 | this->priv->reverse = !this->priv->reverse; 224 | } 225 | 226 | int wf::animation::duration_t::get_direction() 227 | { 228 | return !this->priv->reverse; 229 | } 230 | 231 | wf::animation::timed_transition_t::timed_transition_t( 232 | const duration_t& dur, double start, double end) : duration(dur.priv) 233 | { 234 | this->set(start, end); 235 | } 236 | 237 | void wf::animation::timed_transition_t::restart_with_end(double new_end) 238 | { 239 | this->start = (double)*this; 240 | this->end = new_end; 241 | } 242 | 243 | void wf::animation::timed_transition_t::restart_same_end() 244 | { 245 | this->start = (double)*this; 246 | } 247 | 248 | void wf::animation::timed_transition_t::set(double start, double end) 249 | { 250 | this->start = start; 251 | this->end = end; 252 | } 253 | 254 | void wf::animation::timed_transition_t::flip() 255 | { 256 | std::swap(this->start, this->end); 257 | } 258 | 259 | wf::animation::timed_transition_t::operator double() const 260 | { 261 | double alpha = this->duration->progress(); 262 | return (1 - alpha) * start + alpha * end; 263 | } 264 | 265 | wf::animation::simple_animation_t::simple_animation_t( 266 | std::shared_ptr> length, 267 | smoothing::smooth_function smooth) : 268 | duration_t(length, smooth), 269 | timed_transition_t((duration_t&)*this) 270 | {} 271 | 272 | wf::animation::simple_animation_t::simple_animation_t( 273 | std::shared_ptr> length) : 274 | duration_t(length), timed_transition_t((duration_t&)*this) 275 | {} 276 | 277 | void wf::animation::simple_animation_t::animate(double start, double end) 278 | { 279 | this->set(start, end); 280 | this->duration_t::start(); 281 | } 282 | 283 | void wf::animation::simple_animation_t::animate(double end) 284 | { 285 | this->restart_with_end(end); 286 | this->duration_t::start(); 287 | } 288 | 289 | void wf::animation::simple_animation_t::animate() 290 | { 291 | this->restart_same_end(); 292 | this->duration_t::start(); 293 | } 294 | 295 | namespace wf 296 | { 297 | namespace animation 298 | { 299 | namespace smoothing 300 | { 301 | // Thanks https://github.com/MrRobinOfficial/EasingFunctions 302 | smooth_function ease_out_elastic = [] (double x) -> double 303 | { 304 | float d = 1.0f; 305 | float p = d * 0.6f; 306 | float s; 307 | float a = 0; 308 | 309 | if (x == 0) 310 | { 311 | return 0; 312 | } 313 | 314 | if ((x /= d) == 1) 315 | { 316 | return 1; 317 | } 318 | 319 | if ((a == 0.0f) || (a < std::abs(1.0))) 320 | { 321 | a = 1.0; 322 | s = p * 0.25f; 323 | } else 324 | { 325 | s = p / (2 * std::acos(-1)) * std::asin(1.0 / a); 326 | } 327 | 328 | return (a * std::pow(2, -10 * x) * std::sin((x * d - s) * (2 * std::acos(-1)) / p) + 1.0); 329 | }; 330 | 331 | static const std::map easing_map = { 332 | {"linear", animation::smoothing::linear}, 333 | {"circle", animation::smoothing::circle}, 334 | {"sigmoid", animation::smoothing::sigmoid}, 335 | {"easeOutElastic", animation::smoothing::ease_out_elastic}, 336 | }; 337 | 338 | std::vector get_available_smooth_functions() 339 | { 340 | std::vector result; 341 | for (auto& func : easing_map) 342 | { 343 | result.push_back(func.first); 344 | } 345 | 346 | return result; 347 | } 348 | } // namespace smoothing 349 | } 350 | 351 | namespace option_type 352 | { 353 | template<> 354 | std::optional from_string(const std::string& value) 355 | { 356 | // Format 1: N (backwards compatible fallback) 357 | if (auto val = from_string(value)) 358 | { 359 | return animation_description_t{ 360 | .length_ms = *val, 361 | .easing = animation::smoothing::circle, 362 | .easing_name = "circle" 363 | }; 364 | } 365 | 366 | // Format 2: N 367 | std::istringstream stream(value); 368 | double N; 369 | std::string suffix, easing; 370 | if (!(stream >> N >> suffix)) 371 | { 372 | return {}; 373 | } 374 | 375 | animation_description_t result; 376 | if ((suffix != "ms") && (suffix != "s")) 377 | { 378 | return {}; 379 | } 380 | 381 | if (!(stream >> result.easing_name)) 382 | { 383 | result.easing_name = "circle"; 384 | } 385 | 386 | if (animation::smoothing::easing_map.count(result.easing_name)) 387 | { 388 | result.easing = animation::smoothing::easing_map.at(result.easing_name); 389 | } else if (result.easing_name == "cubic-bezier") 390 | { 391 | double x1 = 0, y1 = 0, x2 = 1, y2 = 1; 392 | stream >> x1 >> y1 >> x2 >> y2; 393 | result.easing = animation::smoothing::get_cubic_bezier(x1, y1, x2, y2); 394 | result.easing_name = "cubic-bezier " + 395 | to_string(x1) + 396 | " " + to_string(y1) + 397 | " " + to_string(x2) + 398 | " " + to_string(y2); 399 | } else 400 | { 401 | return {}; 402 | } 403 | 404 | std::string tmp; 405 | if (stream >> tmp) 406 | { 407 | // Trailing data 408 | return {}; 409 | } 410 | 411 | if (suffix == "s") 412 | { 413 | result.length_ms = N * 1000; 414 | } else 415 | { 416 | result.length_ms = N; 417 | } 418 | 419 | return result; 420 | } 421 | 422 | template<> 423 | std::string to_string(const animation_description_t& value) 424 | { 425 | return to_string(value.length_ms) + "ms " + to_string(value.easing_name); 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /test/option_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "../src/option-impl.hpp" 10 | 11 | /** 12 | * A struct to check whether the maximum and minimum methods are enabled on the 13 | * template parameter class. 14 | */ 15 | template 16 | struct are_bounds_enabled 17 | { 18 | private: 19 | template 20 | static constexpr bool has_bounds( 21 | decltype(&V::template set_maximum<>), 22 | decltype(&V::template set_minimum<>), 23 | decltype(&V::get_maximum), 24 | decltype(&V::get_minimum)) 25 | { 26 | return true; 27 | } 28 | 29 | template 30 | static constexpr bool has_bounds(...) 31 | { 32 | return false; 33 | } 34 | 35 | public: 36 | enum 37 | { 38 | value = has_bounds(nullptr, 39 | nullptr, 40 | nullptr, 41 | nullptr), 42 | }; 43 | }; 44 | 45 | TEST_CASE("wf::config::option_t") 46 | { 47 | using namespace wf; 48 | using namespace wf::config; 49 | 50 | const wf::keybinding_t binding1{KEYBOARD_MODIFIER_ALT, KEY_E}; 51 | const wf::keybinding_t binding2{KEYBOARD_MODIFIER_LOGO, KEY_T}; 52 | 53 | option_t opt("test123", binding1); 54 | CHECK(opt.get_name() == "test123"); 55 | CHECK(opt.get_value() == binding1); 56 | 57 | opt.set_value(binding2); 58 | CHECK(opt.get_value() == binding2); 59 | 60 | opt.set_value(binding1); 61 | CHECK(opt.get_value() == binding1); 62 | CHECK(opt.set_value_str("KEY_T")); 63 | CHECK(opt.get_value() == binding2); 64 | CHECK(!opt.set_value_str("garbage")); 65 | CHECK(opt.get_value() == binding2); 66 | opt.set_value_str("KEY_T"); 67 | CHECK(opt.get_value() == binding2); 68 | opt.reset_to_default(); 69 | CHECK(opt.get_value() == binding1); 70 | 71 | CHECK(wf::option_type::from_string( 72 | opt.get_value_str()).value() == binding1); 73 | 74 | CHECK(opt.set_default_value_str("KEY_T")); 75 | opt.reset_to_default(); 76 | CHECK(opt.get_value() == binding2); 77 | CHECK(opt.get_default_value() == binding2); 78 | CHECK(wf::option_type::from_string( 79 | opt.get_default_value_str()) == binding2); 80 | 81 | CHECK(are_bounds_enabled>::value == false); 82 | CHECK(are_bounds_enabled>::value == false); 83 | CHECK(are_bounds_enabled>::value == false); 84 | CHECK(are_bounds_enabled>::value == false); 85 | 86 | int callback_called = 0, clone_callback_called = 0; 87 | wf::config::option_base_t::updated_callback_t 88 | callback = [&] () { callback_called++; }, clone_callback = [&] () 89 | { 90 | clone_callback_called++; 91 | }; 92 | opt.add_updated_handler(&callback); 93 | opt.priv->xml = (xmlNode*)0x123; 94 | auto clone = std::static_pointer_cast>( 95 | opt.clone_option()); 96 | CHECK(clone->priv->xml == (xmlNode*)0x123); 97 | CHECK(clone->get_name() == opt.get_name()); 98 | CHECK(clone->get_default_value() == opt.get_default_value()); 99 | CHECK(clone->get_value() == opt.get_value()); 100 | opt.set_value_str("KEY_F"); 101 | CHECK(callback_called == 1); 102 | clone->add_updated_handler(&clone_callback); 103 | clone->set_value_str("KEY_F"); 104 | CHECK(callback_called == 1); 105 | CHECK(clone_callback_called == 1); 106 | } 107 | 108 | TEST_CASE("wf::config::option_t") 109 | { 110 | using namespace wf; 111 | using namespace wf::config; 112 | 113 | option_t iopt{"int123", 5}; 114 | CHECK(iopt.get_name() == "int123"); 115 | CHECK(iopt.get_value() == 5); 116 | CHECK(!iopt.get_minimum()); 117 | CHECK(!iopt.get_maximum()); 118 | 119 | iopt.set_minimum(0); 120 | CHECK(!iopt.get_maximum()); 121 | CHECK(iopt.get_minimum().value_or(0) == 0); 122 | 123 | iopt.set_maximum(10); 124 | CHECK(iopt.get_maximum().value_or(11) == 10); 125 | CHECK(iopt.get_minimum().value_or(1) == 0); 126 | 127 | CHECK(iopt.get_value() == 5); 128 | iopt.set_value(8); 129 | CHECK(iopt.get_value() == 8); 130 | iopt.set_value(11); 131 | CHECK(iopt.get_value() == 10); 132 | iopt.set_value(-1); 133 | CHECK(iopt.get_value() == 0); 134 | iopt.set_minimum(3); 135 | CHECK(iopt.get_minimum().value_or(0) == 3); 136 | CHECK(iopt.get_value() == 3); 137 | iopt.reset_to_default(); 138 | CHECK(iopt.get_value() == 5); 139 | CHECK(wf::option_type::from_string(iopt.get_value_str()).value_or(0) == 5); 140 | 141 | option_t dopt{"dbl123", -1.0}; 142 | dopt.set_value(-100); 143 | CHECK(dopt.get_value() == doctest::Approx(-100.0)); 144 | dopt.set_minimum(50); 145 | dopt.set_maximum(50); 146 | CHECK(dopt.get_value() == doctest::Approx(50)); 147 | CHECK(dopt.get_minimum().value_or(60) == doctest::Approx(50)); 148 | CHECK(dopt.get_maximum().value_or(60) == doctest::Approx(50)); 149 | CHECK(wf::option_type::from_string(dopt.get_value_str()).value_or(0) == 150 | doctest::Approx(50)); 151 | 152 | dopt.set_maximum(60); 153 | CHECK(dopt.set_default_value_str("55")); 154 | CHECK(!dopt.set_default_value_str("invalid dobule")); 155 | dopt.reset_to_default(); 156 | CHECK(dopt.get_value() == doctest::Approx(55)); 157 | CHECK(dopt.get_default_value() == doctest::Approx(55)); 158 | 159 | CHECK(dopt.set_default_value_str("75")); // invalid wrt min/max 160 | dopt.reset_to_default(); 161 | CHECK(dopt.get_value() == doctest::Approx(60)); // not more than max 162 | 163 | auto clone = std::static_pointer_cast>(dopt.clone_option()); 164 | CHECK(clone->get_minimum() == dopt.get_minimum()); 165 | CHECK(clone->get_maximum() == dopt.get_maximum()); 166 | 167 | CHECK(are_bounds_enabled>::value); 168 | CHECK(are_bounds_enabled>::value); 169 | } 170 | 171 | TEST_CASE("compound options") 172 | { 173 | using namespace wf; 174 | using namespace wf::config; 175 | 176 | compound_option_t::entries_t entries; 177 | entries.push_back(std::make_unique>("hey_")); 178 | entries.push_back(std::make_unique>("bey_")); 179 | 180 | compound_option_t opt{"Test", std::move(entries)}; 181 | 182 | auto section = std::make_shared("TestSection"); 183 | section->register_new_option(std::make_shared>("hey_k1", 1)); 184 | section->register_new_option(std::make_shared>("hey_k2", -12)); 185 | section->register_new_option(std::make_shared>("bey_k1", 1.2)); 186 | section->register_new_option(std::make_shared>("bey_k2", 187 | 3.1415)); 188 | 189 | // Not fully specified pairs 190 | section->register_new_option(std::make_shared>("hey_k3", 3)); 191 | // One of the values is a regular option with an associated XML tag, and 192 | // needs to be skipped 193 | auto xml_opt = std::make_shared>("bey_k3", 5.5); 194 | xml_opt->priv->xml = (xmlNode*)0x123; 195 | section->register_new_option(xml_opt); 196 | 197 | section->register_new_option(std::make_shared>("bey_k4", 198 | "invalid value")); 199 | section->register_new_option(std::make_shared>("bey_k5", 3.5)); 200 | 201 | // Options which don't match anything 202 | section->register_new_option(std::make_shared>("hallo", 3.5)); 203 | 204 | // Mark all options as coming from the config file, otherwise, they wont' be 205 | // parsed 206 | for (auto& opt : section->get_registered_options()) 207 | { 208 | opt->priv->option_in_config_file = true; 209 | } 210 | 211 | update_compound_from_section(opt, section); 212 | auto values = opt.get_value(); 213 | 214 | REQUIRE(values.size() == 2); 215 | std::sort(values.begin(), values.end()); 216 | 217 | CHECK(std::get<0>(values[0]) == "k1"); 218 | CHECK(std::get<1>(values[0]) == 1); 219 | CHECK(std::get<2>(values[0]) == 1.2); 220 | 221 | CHECK(std::get<0>(values[1]) == "k2"); 222 | CHECK(std::get<1>(values[1]) == -12); 223 | CHECK(std::get<2>(values[1]) == 3.1415); 224 | 225 | std::vector> untyped_values = { 226 | {"k1", "1", "1.200000"}, 227 | {"k2", "-12", "3.141500"}, 228 | }; 229 | 230 | CHECK(opt.get_value_untyped() == untyped_values); 231 | 232 | compound_list_t v = { 233 | {"k3", 1, 1.23} 234 | }; 235 | 236 | opt.set_value(v); 237 | CHECK(v == opt.get_value()); 238 | 239 | compound_option_t::stored_type_t v2 = { 240 | {"k3", "1", "1.23"} 241 | }; 242 | 243 | CHECK(opt.set_value_untyped(v2)); 244 | CHECK(v == opt.get_value()); 245 | 246 | // Fail to set 247 | compound_option_t::stored_type_t v3 = { 248 | {"k3", "1"} 249 | }; 250 | CHECK(!opt.set_value_untyped(v3)); 251 | 252 | compound_option_t::stored_type_t v4 = { 253 | {"k3", "1", "invalid double"} 254 | }; 255 | CHECK(!opt.set_value_untyped(v4)); 256 | } 257 | 258 | TEST_CASE("compound option with default values") 259 | { 260 | using namespace wf; 261 | using namespace wf::config; 262 | 263 | compound_option_t::entries_t entries; 264 | entries.push_back(std::make_unique>("int_", "int", 265 | "42")); 266 | entries.push_back(std::make_unique>("double_", 267 | "double")); 268 | entries.push_back(std::make_unique>("str_", 269 | "str", "default")); 270 | 271 | compound_option_t opt("Test", std::move(entries)); 272 | 273 | auto section = std::make_shared("TestSection"); 274 | // k1 -- all entries are scecified 275 | // k2 -- double value is unspecified (error) 276 | // k3 -- invalid int value, should use the default one 277 | // k4 -- only double value is specified 278 | section->register_new_option(std::make_shared>("int_k1", 1)); 279 | section->register_new_option(std::make_shared>("int_k2", 2)); 280 | section->register_new_option(std::make_shared>("int_k3", 281 | "invalid")); 282 | 283 | section->register_new_option(std::make_shared>("double_k1", 284 | 1.0)); 285 | section->register_new_option(std::make_shared>("double_k3", 286 | 3.0)); 287 | section->register_new_option(std::make_shared>("double_k4", 288 | 4.0)); 289 | 290 | section->register_new_option(std::make_shared>("str_k1", 291 | "s1")); 292 | section->register_new_option(std::make_shared>("str_k2", 293 | "s2")); 294 | section->register_new_option(std::make_shared>("str_k3", 295 | "s3")); 296 | 297 | // Mark all options as coming from the config file, otherwise, they wont' be 298 | // parsed 299 | for (auto& opt : section->get_registered_options()) 300 | { 301 | opt->priv->option_in_config_file = true; 302 | } 303 | 304 | update_compound_from_section(opt, section); 305 | auto values = opt.get_value(); 306 | 307 | REQUIRE(values.size() == 3); 308 | std::sort(values.begin(), values.end()); 309 | 310 | CHECK(values[0] == std::tuple{"k1", 1, 1.0, "s1"}); 311 | CHECK(values[1] == std::tuple{"k3", 42, 3.0, "s3"}); 312 | CHECK(values[2] == std::tuple{"k4", 42, 4.0, "default"}); 313 | } 314 | 315 | TEST_CASE("Plain list compound options") 316 | { 317 | using namespace wf::config; 318 | 319 | compound_option_t::entries_t entries; 320 | entries.push_back(std::make_unique>("hey_")); 321 | entries.push_back(std::make_unique>("bey_")); 322 | compound_option_t opt{"Test", std::move(entries)}; 323 | 324 | simple_list_t simple_list = { 325 | {0, 0.0}, 326 | {1, -1.5} 327 | }; 328 | 329 | opt.set_value_simple(simple_list); 330 | auto with_names = opt.get_value(); 331 | 332 | compound_list_t compound_list = { 333 | {"0", 0, 0.0}, 334 | {"1", 1, -1.5} 335 | }; 336 | CHECK(compound_list == opt.get_value()); 337 | 338 | compound_list = { 339 | {"test", 10, 0.0}, 340 | {"blah", 20, 15.6} 341 | }; 342 | opt.set_value(compound_list); 343 | 344 | simple_list = { 345 | {10, 0.0}, 346 | {20, 15.6} 347 | }; 348 | 349 | CHECK(simple_list == opt.get_value_simple()); 350 | } 351 | -------------------------------------------------------------------------------- /src/xml.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "section-impl.hpp" 7 | #include "option-impl.hpp" 8 | #include "wayfire/util/duration.hpp" 9 | 10 | static std::optional extract_value(xmlNodePtr node, 11 | std::string value_name) 12 | { 13 | std::optional value_ptr; 14 | 15 | auto child_ptr = node->children; 16 | while (child_ptr != nullptr) 17 | { 18 | if ((child_ptr->type == XML_ELEMENT_NODE) && 19 | (std::string((const char*)child_ptr->name) == value_name)) 20 | { 21 | auto child_child_ptr = child_ptr->children; 22 | if (child_child_ptr == nullptr) 23 | { 24 | value_ptr = (const xmlChar*)""; 25 | } else if ((child_child_ptr->next == nullptr) && 26 | (child_child_ptr->type == XML_TEXT_NODE)) 27 | { 28 | value_ptr = child_child_ptr->content; 29 | } 30 | } 31 | 32 | child_ptr = child_ptr->next; 33 | } 34 | 35 | return value_ptr; 36 | } 37 | 38 | /** 39 | * Create a new option of type T with the given name and default value. 40 | * @return The new option, or nullptr if the default value is invaild. 41 | */ 42 | template 43 | std::shared_ptr> create_option(std::string name, 44 | std::string default_value) 45 | { 46 | auto value = wf::option_type::from_string(default_value); 47 | if (!value) 48 | { 49 | return {}; 50 | } 51 | 52 | return std::make_shared>(name, value.value()); 53 | } 54 | 55 | enum bounds_error_t 56 | { 57 | BOUNDS_INVALID_MINIMUM, 58 | BOUNDS_INVALID_MAXIMUM, 59 | BOUNDS_OK, 60 | }; 61 | 62 | template 63 | bounds_error_t set_bounds( 64 | std::shared_ptr& option, 65 | std::optional min_ptr, 66 | std::optional max_ptr) 67 | { 68 | if (!option) 69 | { 70 | return BOUNDS_OK; // there has been an earlier error 71 | } 72 | 73 | auto typed_option = 74 | std::dynamic_pointer_cast>(option); 75 | assert(typed_option); 76 | 77 | if (min_ptr) 78 | { 79 | auto value = wf::option_type::from_string( 80 | (const char*)min_ptr.value()); 81 | if (value) 82 | { 83 | typed_option->set_minimum(value.value()); 84 | } else 85 | { 86 | return BOUNDS_INVALID_MINIMUM; 87 | } 88 | } 89 | 90 | if (max_ptr) 91 | { 92 | std::optional value = wf::option_type::from_string( 93 | (const char*)max_ptr.value()); 94 | if (value) 95 | { 96 | typed_option->set_maximum(value.value()); 97 | } else 98 | { 99 | return BOUNDS_INVALID_MAXIMUM; 100 | } 101 | } 102 | 103 | return BOUNDS_OK; 104 | } 105 | 106 | #define GET_XML_PROP_OR_BAIL(node, name, str) \ 107 | const char *name ## _ptr = (const char*)xmlGetProp(node, (const xmlChar*)(str)); \ 108 | if (!name ## _ptr) \ 109 | { \ 110 | LOGE("Could not parse ", (node)->doc->URL, \ 111 | ": XML node at line ", node->line, " is missing \"" #name "\" attribute."); \ 112 | return nullptr; \ 113 | } \ 114 | std::string name = name ## _ptr; 115 | 116 | #define GET_OPTIONAL_XML_PROP(node, name, str) \ 117 | const char *name ## _ptr = (const char*)xmlGetProp(node, (const xmlChar*)(str)); \ 118 | if (!name ## _ptr) \ 119 | { \ 120 | name ## _ptr = ""; \ 121 | } \ 122 | std::string name = name ## _ptr; 123 | 124 | template 125 | using entry_t = wf::config::compound_option_entry_t; 126 | 127 | std::shared_ptr parse_compound_option(xmlNodePtr node, 128 | const std::string& name) 129 | { 130 | wf::config::compound_option_t::entries_t entries; 131 | GET_OPTIONAL_XML_PROP(node, type_hint, "type-hint"); 132 | 133 | if (type_hint.empty()) 134 | { 135 | type_hint = "dict"; 136 | } 137 | 138 | node = node->children; 139 | while (node) 140 | { 141 | if ((node->type == XML_ELEMENT_NODE) && 142 | ((const char*)node->name == std::string{"entry"})) 143 | { 144 | // Found next item 145 | GET_XML_PROP_OR_BAIL(node, prefix, "prefix"); 146 | GET_XML_PROP_OR_BAIL(node, type, "type"); 147 | GET_OPTIONAL_XML_PROP(node, name, "name"); 148 | 149 | std::optional default_value = std::nullopt; 150 | if (const auto& default_value_raw = extract_value(node, "default")) 151 | { 152 | default_value = (const char*)default_value_raw.value(); 153 | } 154 | 155 | if (type == "int") 156 | { 157 | entries.push_back(std::make_unique>(prefix, name, 158 | default_value)); 159 | } else if (type == "double") 160 | { 161 | entries.push_back(std::make_unique>(prefix, name, 162 | default_value)); 163 | } else if (type == "bool") 164 | { 165 | entries.push_back(std::make_unique>(prefix, name, 166 | default_value)); 167 | } else if (type == "string") 168 | { 169 | entries.push_back(std::make_unique>(prefix, 170 | name, default_value)); 171 | } else if (type == "key") 172 | { 173 | entries.push_back(std::make_unique>(prefix, 174 | name, default_value)); 175 | } else if (type == "button") 176 | { 177 | entries.push_back(std::make_unique>( 178 | prefix, name, default_value)); 179 | } else if (type == "gesture") 180 | { 181 | entries.push_back(std::make_unique>( 182 | prefix, name, default_value)); 183 | } else if (type == "color") 184 | { 185 | entries.push_back(std::make_unique>(prefix, 186 | name, default_value)); 187 | } else if (type == "activator") 188 | { 189 | entries.push_back(std::make_unique>( 190 | prefix, name, default_value)); 191 | } else if (type == "animation") 192 | { 193 | entries.push_back(std::make_unique>( 194 | prefix, name, default_value)); 195 | } else 196 | { 197 | LOGE("Could not parse ", node->doc->URL, 198 | ": option at line ", node->line, 199 | " has invalid type \"", type, "\""); 200 | return nullptr; 201 | } 202 | } 203 | 204 | node = node->next; 205 | } 206 | 207 | auto opt = 208 | new wf::config::compound_option_t{name, std::move(entries), type_hint}; 209 | return std::shared_ptr(opt); 210 | } 211 | 212 | std::shared_ptr wf::config::xml::create_option_from_xml_node(xmlNodePtr node) 213 | { 214 | if ((node->type != XML_ELEMENT_NODE) || 215 | ((const char*)node->name != std::string{"option"})) 216 | { 217 | LOGE("Could not parse ", node->doc->URL, 218 | ": line ", node->line, " is not an option element."); 219 | return nullptr; 220 | } 221 | 222 | GET_XML_PROP_OR_BAIL(node, name, "name"); 223 | GET_XML_PROP_OR_BAIL(node, type, "type"); 224 | if (type == "dynamic-list") 225 | { 226 | auto option = parse_compound_option(node, name); 227 | if (option) 228 | { 229 | option->priv->xml = node; 230 | } 231 | 232 | return option; 233 | } 234 | 235 | auto default_value_ptr = extract_value(node, "default"); 236 | if (!default_value_ptr) 237 | { 238 | LOGE("Could not parse ", node->doc->URL, 239 | ": option at line ", node->line, " has no default value specified."); 240 | return nullptr; 241 | } 242 | 243 | std::string default_value = (const char*)default_value_ptr.value(); 244 | 245 | auto min_value_ptr = extract_value(node, "min"); 246 | auto max_value_ptr = extract_value(node, "max"); 247 | 248 | std::shared_ptr option; 249 | bounds_error_t bounds_error = BOUNDS_OK; 250 | 251 | if (type == "int") 252 | { 253 | option = create_option(name, default_value); 254 | bounds_error = set_bounds(option, 255 | min_value_ptr, max_value_ptr); 256 | } else if (type == "double") 257 | { 258 | option = create_option(name, default_value); 259 | bounds_error = set_bounds(option, 260 | min_value_ptr, max_value_ptr); 261 | } else if (type == "bool") 262 | { 263 | option = create_option(name, default_value); 264 | } else if (type == "string") 265 | { 266 | option = create_option(name, default_value); 267 | } else if (type == "key") 268 | { 269 | option = create_option(name, default_value); 270 | } else if (type == "button") 271 | { 272 | option = create_option(name, default_value); 273 | } else if (type == "gesture") 274 | { 275 | option = create_option(name, default_value); 276 | } else if (type == "color") 277 | { 278 | option = create_option(name, default_value); 279 | } else if (type == "activator") 280 | { 281 | option = create_option(name, default_value); 282 | } else if (type == "output::mode") 283 | { 284 | option = create_option(name, default_value); 285 | } else if (type == "output::position") 286 | { 287 | option = create_option(name, default_value); 288 | } else if (type == "animation") 289 | { 290 | option = create_option(name, default_value); 291 | } else 292 | { 293 | LOGE("Could not parse ", node->doc->URL, 294 | ": option at line ", node->line, 295 | " has invalid type \"", type, "\""); 296 | return nullptr; 297 | } 298 | 299 | if (!option) 300 | { 301 | /* This can only happen if default value was invalid */ 302 | LOGE("Could not parse ", node->doc->URL, 303 | ": option at line ", node->line, 304 | " has invalid default value \"", default_value, "\" for type ", 305 | type); 306 | return nullptr; 307 | } 308 | 309 | switch (bounds_error) 310 | { 311 | case BOUNDS_INVALID_MINIMUM: 312 | assert(min_value_ptr); 313 | LOGE("Could not parse ", node->doc->URL, 314 | ": option at line ", node->line, 315 | " has invalid minimum value \"", min_value_ptr.value(), "\"", 316 | "for type ", type); 317 | return nullptr; 318 | 319 | case BOUNDS_INVALID_MAXIMUM: 320 | assert(max_value_ptr); 321 | LOGE("Could not parse ", node->doc->URL, 322 | ": option at line ", node->line, 323 | " has invalid maximum value \"", max_value_ptr.value(), "\"", 324 | "for type ", type); 325 | return nullptr; 326 | 327 | default: 328 | break; 329 | } 330 | 331 | option->priv->xml = node; 332 | return option; 333 | } 334 | 335 | static void recursively_parse_section_node(xmlNodePtr node, 336 | std::shared_ptr section) 337 | { 338 | auto child_ptr = node->children; 339 | while (child_ptr != nullptr) 340 | { 341 | if ((child_ptr->type == XML_ELEMENT_NODE) && 342 | (std::string((const char*)child_ptr->name) == "option")) 343 | { 344 | auto option = wf::config::xml::create_option_from_xml_node( 345 | child_ptr); 346 | if (option) 347 | { 348 | section->register_new_option(option); 349 | } 350 | } 351 | 352 | if ((child_ptr->type == XML_ELEMENT_NODE) && 353 | (std::string((const char*)child_ptr->name) == "group")) 354 | { 355 | recursively_parse_section_node(child_ptr, section); 356 | } 357 | 358 | if ((child_ptr->type == XML_ELEMENT_NODE) && 359 | (std::string((const char*)child_ptr->name) == "subgroup")) 360 | { 361 | recursively_parse_section_node(child_ptr, section); 362 | } 363 | 364 | child_ptr = child_ptr->next; 365 | } 366 | } 367 | 368 | std::shared_ptr wf::config::xml::create_section_from_xml_node( 369 | xmlNodePtr node) 370 | { 371 | if ((node->type != XML_ELEMENT_NODE) || 372 | (((const char*)node->name != std::string{"plugin"}) && 373 | ((const char*)node->name != std::string{"object"}))) 374 | { 375 | LOGE("Could not parse ", node->doc->URL, 376 | ": line ", node->line, " is not a plugin/object element."); 377 | return nullptr; 378 | } 379 | 380 | GET_XML_PROP_OR_BAIL(node, name, "name"); 381 | auto section = std::make_shared(name); 382 | section->priv->xml = node; 383 | recursively_parse_section_node(node, section); 384 | return section; 385 | } 386 | 387 | xmlNodePtr wf::config::xml::get_option_xml_node( 388 | std::shared_ptr option) 389 | { 390 | return option->priv->xml; 391 | } 392 | 393 | xmlNodePtr wf::config::xml::get_section_xml_node( 394 | std::shared_ptr section) 395 | { 396 | return section->priv->xml; 397 | } 398 | -------------------------------------------------------------------------------- /test/file_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include "wayfire/config/compound-option.hpp" 14 | #include "../src/option-impl.hpp" 15 | 16 | const std::string contents = 17 | R"( 18 | illegal_option = value 19 | 20 | [section1] 21 | option1 = value1 22 | option2=3 23 | #Comment 24 | option3 = value value value # Comment 25 | 26 | hey_a = 15 27 | bey_a = 1.2 28 | 29 | [section2] 30 | option1 = value 4 \ 31 | value # Ignore 32 | option2 = value \\ 33 | # Ignore 34 | option3 = \#No way 35 | 36 | [wrongsection 37 | option1 38 | )"; 39 | 40 | #include "expect_line.hpp" 41 | 42 | 43 | TEST_CASE("wf::config::load_configuration_options_from_string") 44 | { 45 | std::stringstream log; 46 | wf::log::initialize_logging(log, wf::log::LOG_LEVEL_DEBUG, 47 | wf::log::LOG_COLOR_MODE_OFF); 48 | 49 | using wf::config::compound_option_t; 50 | using wf::config::compound_option_entry_t; 51 | 52 | compound_option_t::entries_t entries; 53 | entries.push_back(std::make_unique>("hey_")); 54 | entries.push_back(std::make_unique>("bey_")); 55 | auto opt = new compound_option_t{"option_list", std::move(entries)}; 56 | 57 | using namespace wf; 58 | using namespace wf::config; 59 | config_manager_t config; 60 | 61 | /* Create the first section and add an option there */ 62 | auto section = std::make_shared("section1"); 63 | section->register_new_option( 64 | std::make_shared>("option1", 10)); 65 | section->register_new_option( 66 | std::make_shared>("option2", 5)); 67 | section->register_new_option( 68 | std::make_shared>("option4", std::string("option4"))); 69 | section->register_new_option(std::shared_ptr(opt)); 70 | 71 | config.merge_section(section); 72 | load_configuration_options_from_string(config, contents, "test"); 73 | 74 | REQUIRE(config.get_section("section1")); 75 | REQUIRE(config.get_section("section2")); 76 | CHECK(config.get_section("wrongsection") == nullptr); 77 | 78 | auto s1 = config.get_section("section1"); 79 | auto s2 = config.get_section("section2"); 80 | 81 | CHECK(s1->get_option("option1")->get_value_str() == "10"); 82 | CHECK(s1->get_option("option2")->get_value_str() == "3"); 83 | CHECK(s1->get_option("option3")->get_value_str() == "value value value"); 84 | CHECK(s1->get_option("option4")->get_value_str() == "option4"); 85 | 86 | CHECK(s2->get_option("option1")->get_value_str() == "value 4 value"); 87 | CHECK(s2->get_option("option2")->get_value_str() == "value \\"); 88 | CHECK(s2->get_option("option3")->get_value_str() == "#No way"); 89 | CHECK(!s2->get_option_or("Ignored")); 90 | 91 | CHECK(opt->get_value().size() == 1); 92 | 93 | EXPECT_LINE(log, "Error in file test:2"); 94 | EXPECT_LINE(log, "Error in file test:5"); 95 | EXPECT_LINE(log, "Error in file test:20"); 96 | EXPECT_LINE(log, "Error in file test:21"); 97 | 98 | // reset logging state for subsequent tests 99 | wf::log::initialize_logging(std::cout, wf::log::LOG_LEVEL_DEBUG, 100 | wf::log::LOG_COLOR_MODE_OFF); 101 | } 102 | 103 | TEST_CASE("wf::config::load_configuration_options_from_string - " 104 | "ignore compound entries not in string") 105 | { 106 | using namespace wf; 107 | using namespace wf::config; 108 | 109 | // Compound option with prefix `test_` 110 | compound_option_t::entries_t entries; 111 | entries.push_back(std::make_unique>("test_")); 112 | auto opt = new compound_option_t{"option_list", std::move(entries)}; 113 | 114 | config_manager_t config; 115 | 116 | // Register compound option 117 | auto section = std::make_shared("section"); 118 | section->register_new_option(std::shared_ptr(opt)); 119 | 120 | // Register an option which does not come from the config file 121 | section->register_new_option( 122 | std::make_shared>("test_nofile", "")); 123 | config.merge_section(section); 124 | 125 | // Update with an emtpy string. The compoud option should remain empty, because 126 | // there are no values from the config file. 127 | load_configuration_options_from_string(config, ""); 128 | CHECK(opt->get_value_untyped().empty()); 129 | } 130 | 131 | const std::string minimal_config_with_opt = R"( 132 | [section] 133 | option = value 134 | )"; 135 | 136 | TEST_CASE("wf::config::load_configuration_options_from_string - lock & reload") 137 | { 138 | using namespace wf; 139 | using namespace wf::config; 140 | 141 | config_manager_t cfg; 142 | load_configuration_options_from_string(cfg, minimal_config_with_opt); 143 | 144 | SUBCASE("locked") 145 | { 146 | cfg.get_option("section/option")->set_locked(); 147 | load_configuration_options_from_string(cfg, ""); 148 | CHECK(cfg.get_option("section/option")->get_value_str() == "value"); 149 | } 150 | 151 | SUBCASE("unlocked") 152 | { 153 | load_configuration_options_from_string(cfg, ""); 154 | CHECK(cfg.get_option("section/option")->get_value_str() == ""); 155 | } 156 | } 157 | 158 | wf::config::config_manager_t build_simple_config() 159 | { 160 | using namespace wf; 161 | using namespace wf::config; 162 | auto section1 = std::make_shared("section1"); 163 | auto section2 = std::make_shared("section2"); 164 | 165 | section1->register_new_option(std::make_shared>("option1", 4)); 166 | section1->register_new_option(std::make_shared>("option2", 167 | std::string("45 # 46 \\"))); 168 | section2->register_new_option(std::make_shared>("option1", 169 | 4.25)); 170 | 171 | compound_option_t::entries_t entries; 172 | entries.push_back(std::make_unique>("hey_")); 173 | entries.push_back(std::make_unique>("bey_")); 174 | auto opt = new compound_option_t{"option_list", std::move(entries)}; 175 | opt->set_value(compound_list_t{{"k1", 1, 1.2}}); 176 | section2->register_new_option(std::shared_ptr(opt)); 177 | 178 | config_manager_t config; 179 | config.merge_section(section1); 180 | config.merge_section(section2); 181 | 182 | return config; 183 | } 184 | 185 | std::string simple_config_source = 186 | R"([section1] 187 | option1 = 4 188 | option2 = 45 \# 46 \\ 189 | 190 | [section2] 191 | bey_k1 = 1.200000 192 | hey_k1 = 1 193 | option1 = 4.250000 194 | 195 | )"; 196 | 197 | 198 | TEST_CASE("wf::config::save_configuration_options_to_string") 199 | { 200 | auto config = build_simple_config(); 201 | auto stringified = save_configuration_options_to_string(config); 202 | CHECK(stringified == simple_config_source); 203 | } 204 | 205 | TEST_CASE("wf::config::save_configuration_options_to_string - compound options erase") 206 | { 207 | using namespace wf; 208 | using namespace wf::config; 209 | 210 | compound_option_t::entries_t entries; 211 | entries.push_back(std::make_unique>("hey_")); 212 | entries.push_back(std::make_unique>("bey_")); 213 | auto opt = new compound_option_t{"option_list", std::move(entries)}; 214 | opt->set_value(compound_list_t{{"k1", 1, 1.2}}); 215 | 216 | auto section = std::make_shared("Section"); 217 | section->register_new_option(std::shared_ptr(opt)); 218 | 219 | // Add the same entries as in the compound option 220 | section->register_new_option(std::make_shared>("hey_k1", 221 | "1")); 222 | section->register_new_option(std::make_shared>("bey_k1", 223 | "1.2")); 224 | 225 | // However, make sure that XML-created options are saved even if they match 226 | // the prefix of a compound option. 227 | auto special_opt = std::make_shared>("hey_you", 1); 228 | special_opt->priv->xml = (xmlNode*)0x123; 229 | section->register_new_option(special_opt); 230 | 231 | config_manager_t cfg; 232 | cfg.merge_section(section); 233 | 234 | // Now, clear the value from the compound option 235 | opt->set_value(compound_list_t{}); 236 | 237 | auto str = save_configuration_options_to_string(cfg); 238 | // We expect that after deleting the values from the compound option, 239 | // the values for k1 are not saved to the string. 240 | const std::string expected = 241 | R"([Section] 242 | hey_you = 1 243 | 244 | )"; 245 | 246 | CHECK(str == expected); 247 | } 248 | 249 | TEST_CASE("wf::config::load_configuration_options_from_file - no such file") 250 | { 251 | std::string test_config = std::string("FileDoesNotExist"); 252 | wf::config::config_manager_t manager; 253 | CHECK(!load_configuration_options_from_file(manager, test_config)); 254 | } 255 | 256 | TEST_CASE("wf::config::load_configuration_options_from_file - locking fails") 257 | { 258 | std::string test_config = std::string("../test/config_lock.ini"); 259 | 260 | const int delay = 100e3; /** 100ms */ 261 | 262 | int pid = fork(); 263 | if (pid == 0) 264 | { 265 | /* Lock config file before parent tries to lock it */ 266 | int fd = open(test_config.c_str(), O_RDWR); 267 | flock(fd, LOCK_EX); 268 | 269 | /* Obtained a lock. Now wait until parent tries to lock */ 270 | usleep(2 * delay); 271 | 272 | /* By now, parent should have failed. */ 273 | flock(fd, LOCK_UN); 274 | close(fd); 275 | } 276 | 277 | /* Wait for other process to lock the file */ 278 | usleep(delay); 279 | 280 | wf::config::config_manager_t manager; 281 | CHECK(!load_configuration_options_from_file(manager, test_config)); 282 | } 283 | 284 | void check_int_test_config(const wf::config::config_manager_t& manager, 285 | std::string value_opt1 = "12") 286 | { 287 | auto s1 = manager.get_section("section1"); 288 | auto s2 = manager.get_section("section2"); 289 | REQUIRE(s1 != nullptr); 290 | REQUIRE(s2 != nullptr); 291 | 292 | auto o1 = manager.get_option("section1/option1"); 293 | auto o2 = manager.get_option("section2/option2"); 294 | auto o3 = manager.get_option("section2/option3"); 295 | 296 | REQUIRE(o1); 297 | REQUIRE(o2); 298 | REQUIRE(o3); 299 | CHECK(o1->get_value_str() == value_opt1); 300 | CHECK(o2->get_value_str() == "opt2"); 301 | CHECK(o3->get_value_str() == "DoesNotExistInXML # \\"); 302 | } 303 | 304 | TEST_CASE("wf::config::load_configuration_options_from_file - success") 305 | { 306 | std::string test_config = std::string(TEST_SOURCE "/int_test/config.ini"); 307 | 308 | /* Init with one section */ 309 | wf::config::config_manager_t manager; 310 | auto s = std::make_shared("section1"); 311 | s->register_new_option( 312 | std::make_shared>("option1", 1)); 313 | manager.merge_section(s); 314 | 315 | CHECK(load_configuration_options_from_file(manager, test_config)); 316 | REQUIRE(manager.get_section("section1") == s); 317 | check_int_test_config(manager); 318 | } 319 | 320 | TEST_CASE("wf::config::save_configuration_to_file - success") 321 | { 322 | std::string test_config = std::string(TEST_SOURCE "/dummy.ini"); 323 | 324 | { 325 | std::ofstream clr(test_config, std::ios::trunc | std::ios::ate); 326 | clr << "Dummy"; 327 | } 328 | 329 | wf::config::save_configuration_to_file(build_simple_config(), test_config); 330 | 331 | /* Read file contents */ 332 | std::ifstream infile(test_config); 333 | std::string file_contents((std::istreambuf_iterator(infile)), 334 | std::istreambuf_iterator()); 335 | 336 | CHECK(file_contents == simple_config_source); 337 | 338 | /* Check lock is released */ 339 | int fd = open(test_config.c_str(), O_RDWR); 340 | CHECK(flock(fd, LOCK_EX | LOCK_NB) == 0); 341 | flock(fd, LOCK_UN); 342 | close(fd); 343 | } 344 | 345 | TEST_CASE("wf::config::build_configuration") 346 | { 347 | wf::log::initialize_logging(std::cout, wf::log::LOG_LEVEL_DEBUG, 348 | wf::log::LOG_COLOR_MODE_ON); 349 | std::string xmldir = std::string(TEST_SOURCE "/int_test/xml"); 350 | std::string sysconf = std::string(TEST_SOURCE "/int_test/sys.ini"); 351 | std::string userconf = std::string(TEST_SOURCE "/int_test/config.ini"); 352 | 353 | std::vector xmldirs(1, xmldir); 354 | auto config = wf::config::build_configuration(xmldirs, sysconf, userconf); 355 | check_int_test_config(config, "10"); 356 | 357 | auto o1 = config.get_option("section1/option1"); 358 | auto o2 = config.get_option("section2/option2"); 359 | auto o3 = config.get_option("section2/option3"); 360 | auto o4 = config.get_option("section2/option4"); 361 | auto o5 = config.get_option("section2/option5"); 362 | auto o6 = config.get_option("sectionobj:objtest/option6"); 363 | 364 | REQUIRE(o4); 365 | REQUIRE(o5); 366 | 367 | using namespace wf; 368 | using namespace wf::config; 369 | CHECK(std::dynamic_pointer_cast>(o1) != nullptr); 370 | CHECK(std::dynamic_pointer_cast>(o2) != nullptr); 371 | CHECK(std::dynamic_pointer_cast>(o3) != nullptr); 372 | CHECK(std::dynamic_pointer_cast>(o4) != nullptr); 373 | CHECK(std::dynamic_pointer_cast>(o5) != nullptr); 374 | CHECK(std::dynamic_pointer_cast>(o6) != nullptr); 375 | 376 | CHECK(o4->get_value_str() == "DoesNotExistInConfig"); 377 | CHECK(o5->get_value_str() == "Option5Sys"); 378 | CHECK(o6->get_value_str() == "10"); // bounds from xml applied 379 | 380 | o1->reset_to_default(); 381 | o2->reset_to_default(); 382 | o3->reset_to_default(); 383 | o4->reset_to_default(); 384 | o5->reset_to_default(); 385 | o6->reset_to_default(); 386 | 387 | CHECK(o1->get_value_str() == "4"); 388 | CHECK(o2->get_value_str() == "XMLDefault"); 389 | CHECK(o3->get_value_str() == ""); 390 | CHECK(o4->get_value_str() == "DoesNotExistInConfig"); 391 | CHECK(o5->get_value_str() == "Option5Sys"); 392 | CHECK(o6->get_value_str() == "1"); 393 | } 394 | -------------------------------------------------------------------------------- /test/xml_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | static const std::string xml_option_int = 13 | R"( 14 | 19 | )"; 20 | 21 | static const std::string xml_option_string = 22 | R"( 23 | 26 | )"; 27 | 28 | static const std::string xml_option_key = 29 | R"( 30 | 33 | )"; 34 | 35 | static const std::string xml_option_dyn_list = 36 | R"( 37 | 42 | )"; 43 | 44 | static const std::string xml_option_dyn_list_no_prefix = 45 | R"( 46 | 49 | )"; 50 | 51 | static const std::string xml_option_dyn_list_no_type = 52 | R"( 53 | 56 | )"; 57 | 58 | static const std::string xml_option_dyn_list_wrong_type = 59 | R"( 60 | 63 | )"; 64 | 65 | static const std::string xml_option_bad_tag = 66 | R"( 67 | 68 | <super> KEY_E 69 | 70 | )"; 71 | 72 | static const std::string xml_option_int_bad_min = 73 | R"( 74 | 79 | )"; 80 | 81 | static const std::string xml_option_int_bad_max = 82 | R"( 83 | 88 | )"; 89 | 90 | static const std::string xml_option_bad_type = 91 | R"( 92 | 95 | )"; 96 | 97 | static const std::string xml_option_bad_default = 98 | R"( 99 | 102 | )"; 103 | 104 | 105 | static const std::string xml_option_missing_name = 106 | R"( 107 | 109 | )"; 110 | 111 | static const std::string xml_option_missing_type = 112 | R"( 113 | 115 | )"; 116 | 117 | static const std::string xml_option_missing_default_value = 118 | R"( 119 | 121 | )"; 122 | 123 | static const std::string xml_option_dyn_list_default = 124 | R"( 125 | 131 | )"; 132 | 133 | #include "expect_line.hpp" 134 | 135 | TEST_CASE("wf::config::xml::create_option") 136 | { 137 | std::stringstream log; 138 | wf::log::initialize_logging(log, 139 | wf::log::LOG_LEVEL_DEBUG, wf::log::LOG_COLOR_MODE_OFF); 140 | 141 | namespace wxml = wf::config::xml; 142 | namespace wc = wf::config; 143 | xmlNodePtr option_node; 144 | 145 | auto initialize_option = [&] (std::string source) 146 | { 147 | auto node = xmlParseDoc((const xmlChar*)source.c_str()); 148 | REQUIRE(node != nullptr); 149 | option_node = xmlDocGetRootElement(node); 150 | return wxml::create_option_from_xml_node(option_node); 151 | }; 152 | 153 | SUBCASE("Not an XML option") 154 | { 155 | auto opt = std::make_shared>("Test", 1); 156 | CHECK(wxml::get_option_xml_node(opt) == nullptr); 157 | } 158 | 159 | SUBCASE("IntOption") 160 | { 161 | auto option = initialize_option(xml_option_int); 162 | REQUIRE(option != nullptr); 163 | 164 | CHECK(option->get_name() == "IntOption"); 165 | 166 | auto as_int = 167 | std::dynamic_pointer_cast>(option); 168 | REQUIRE(as_int); 169 | REQUIRE(as_int->get_minimum()); 170 | REQUIRE(as_int->get_maximum()); 171 | 172 | CHECK(as_int->get_value() == 3); 173 | CHECK(as_int->get_minimum().value() == 0); 174 | CHECK(as_int->get_maximum().value() == 10); 175 | CHECK(wxml::get_option_xml_node(as_int) == option_node); 176 | } 177 | 178 | SUBCASE("StringOption") 179 | { 180 | auto option = initialize_option(xml_option_string); 181 | REQUIRE(option != nullptr); 182 | 183 | CHECK(option->get_name() == "StringOption"); 184 | 185 | auto as_string = 186 | std::dynamic_pointer_cast>(option); 187 | REQUIRE(as_string); 188 | CHECK(as_string->get_value() == ""); 189 | } 190 | 191 | SUBCASE("KeyOption") 192 | { 193 | auto option = initialize_option(xml_option_key); 194 | REQUIRE(option != nullptr); 195 | 196 | CHECK(option->get_name() == "KeyOption"); 197 | 198 | auto as_key = 199 | std::dynamic_pointer_cast>(option); 200 | REQUIRE(as_key); 201 | 202 | CHECK(as_key->get_value() == 203 | wf::keybinding_t{wf::KEYBOARD_MODIFIER_LOGO, KEY_E}); 204 | CHECK(wxml::get_option_xml_node(option) == option_node); 205 | } 206 | 207 | SUBCASE("DynamicList") 208 | { 209 | auto option = initialize_option(xml_option_dyn_list); 210 | REQUIRE(option != nullptr); 211 | 212 | CHECK(option->get_name() == "DynList"); 213 | 214 | auto as_co = 215 | std::dynamic_pointer_cast(option); 216 | REQUIRE(as_co != nullptr); 217 | 218 | CHECK(as_co->get_value() == 219 | wf::config::compound_list_t{}); 220 | 221 | const auto& entries = as_co->get_entries(); 222 | REQUIRE(entries.size() == 2); 223 | CHECK( 224 | dynamic_cast*>(entries[0].get())); 225 | CHECK(dynamic_cast*>( 226 | entries[1].get())); 227 | } 228 | 229 | SUBCASE("DynamicListDefaultOption") 230 | { 231 | auto option = initialize_option(xml_option_dyn_list_default); 232 | REQUIRE(option != nullptr); 233 | 234 | auto as_co = std::dynamic_pointer_cast(option); 235 | REQUIRE(as_co != nullptr); 236 | 237 | CHECK(as_co->get_value() == 238 | wf::config::compound_list_t{}); 239 | 240 | const auto& entries = as_co->get_entries(); 241 | REQUIRE(entries.size() == 2); 242 | CHECK(dynamic_cast*>(entries[0].get())); 243 | REQUIRE(entries[0]->get_default_value() == "9"); 244 | CHECK( 245 | dynamic_cast*>(entries[1].get())); 246 | REQUIRE(entries[1]->get_default_value() == std::nullopt); 247 | } 248 | 249 | /* Generate a subcase where the given xml source can't be parsed to an 250 | * option, and check that the output in the log is as expected. */ 251 | #define SUBCASE_BAD_OPTION(subcase_name, xml_source, expected_log) \ 252 | SUBCASE(subcase_name) \ 253 | { \ 254 | auto option = initialize_option(xml_source); \ 255 | CHECK(option == nullptr); \ 256 | EXPECT_LINE(log, expected_log); \ 257 | } 258 | 259 | SUBCASE_BAD_OPTION("Invalid xml tag", 260 | xml_option_bad_tag, "is not an option element"); 261 | 262 | SUBCASE_BAD_OPTION("Invalid option type", 263 | xml_option_bad_type, "invalid type \"unknown\""); 264 | 265 | SUBCASE_BAD_OPTION("Invalid default value", 266 | xml_option_bad_default, "invalid default value"); 267 | 268 | SUBCASE_BAD_OPTION("Invalid minimum value", 269 | xml_option_int_bad_min, "invalid minimum value"); 270 | 271 | SUBCASE_BAD_OPTION("Invalid maximum value", 272 | xml_option_int_bad_max, "invalid maximum value"); 273 | 274 | SUBCASE_BAD_OPTION("Missing option name", 275 | xml_option_missing_name, "missing \"name\" attribute"); 276 | 277 | SUBCASE_BAD_OPTION("Missing option type", 278 | xml_option_missing_type, "missing \"type\" attribute"); 279 | 280 | SUBCASE_BAD_OPTION("Missing option default value", 281 | xml_option_missing_default_value, "no default value specified"); 282 | 283 | SUBCASE_BAD_OPTION("Dynamic list without prefix", 284 | xml_option_dyn_list_no_prefix, "missing \"prefix\" attribute"); 285 | 286 | SUBCASE_BAD_OPTION("Dynamic list without type", 287 | xml_option_dyn_list_no_type, "missing \"type\" attribute"); 288 | 289 | SUBCASE_BAD_OPTION("Dynamic list with invalid type", 290 | xml_option_dyn_list_wrong_type, "invalid type"); 291 | } 292 | 293 | /* ------------------------- create_section test ---------------------------- */ 294 | static const std::string xml_section_empty = 295 | R"( 296 | 297 | 298 | )"; 299 | 300 | static const std::string xml_section_no_plugins = 301 | R"( 302 | 303 | 304 | 305 | 306 | )"; 307 | 308 | static const std::string xml_section_full = 309 | R"( 310 | 311 | 314 | 317 | 320 | 323 | 326 | 329 | 332 | 333 | 336 | 339 | 340 | 343 | 344 | 347 | 348 | 349 | )"; 350 | 351 | static const std::string xml_section_missing_name = 352 | R"( 353 | 354 | 357 | 358 | )"; 359 | 360 | static const std::string xml_section_bad_tag = 361 | R"( 362 | 363 | 366 | 367 | )"; 368 | 369 | TEST_CASE("wf::config::xml::create_section") 370 | { 371 | std::stringstream log; 372 | wf::log::initialize_logging(log, 373 | wf::log::LOG_LEVEL_DEBUG, wf::log::LOG_COLOR_MODE_OFF); 374 | 375 | namespace wxml = wf::config::xml; 376 | namespace wc = wf::config; 377 | 378 | xmlNodePtr section_root; 379 | auto initialize_section = [&] (std::string xml_source) 380 | { 381 | auto node = xmlParseDoc((const xmlChar*)xml_source.c_str()); 382 | REQUIRE(node != nullptr); 383 | section_root = xmlDocGetRootElement(node); 384 | return wxml::create_section_from_xml_node(section_root); 385 | }; 386 | 387 | SUBCASE("Section without XML") 388 | { 389 | auto section = std::make_shared("TestSection"); 390 | CHECK(wxml::get_section_xml_node(section) == nullptr); 391 | } 392 | 393 | SUBCASE("Empty section") 394 | { 395 | auto section = initialize_section(xml_section_empty); 396 | REQUIRE(section != nullptr); 397 | CHECK(section->get_name() == "TestPluginEmpty"); 398 | CHECK(section->get_registered_options().empty()); 399 | CHECK(wxml::get_section_xml_node(section) == section_root); 400 | } 401 | 402 | SUBCASE("Empty section - unnecessary data") 403 | { 404 | auto section = initialize_section(xml_section_no_plugins); 405 | REQUIRE(section != nullptr); 406 | CHECK(section->get_name() == "TestPluginNoPlugins"); 407 | CHECK(section->get_registered_options().empty()); 408 | CHECK(wxml::get_section_xml_node(section) == section_root); 409 | } 410 | 411 | SUBCASE("Section with options") 412 | { 413 | auto section = initialize_section(xml_section_full); 414 | REQUIRE(section != nullptr); 415 | CHECK(section->get_name() == "TestPluginFull"); 416 | 417 | auto opts = section->get_registered_options(); 418 | std::set opt_names; 419 | for (auto& opt : opts) 420 | { 421 | opt_names.insert(opt->get_name()); 422 | } 423 | 424 | std::set expected_names = { 425 | "KeyOption", "ButtonOption", "TouchOption", "ActivatorOption", 426 | "IntOption", "DoubleOption", "BoolOption", "StringOption", 427 | "OutputModeOption", "OutputPositionOption"}; 428 | CHECK(opt_names == expected_names); 429 | CHECK(wxml::get_section_xml_node(section) == section_root); 430 | } 431 | 432 | SUBCASE("Missing section name") 433 | { 434 | auto section = initialize_section(xml_section_missing_name); 435 | CHECK(section == nullptr); 436 | EXPECT_LINE(log, "missing \"name\" attribute"); 437 | } 438 | 439 | SUBCASE("Invalid section xml tag") 440 | { 441 | auto section = initialize_section(xml_section_bad_tag); 442 | CHECK(section == nullptr); 443 | EXPECT_LINE(log, "is not a plugin/object element"); 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /include/wayfire/config/types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace wf 9 | { 10 | namespace option_type 11 | { 12 | /** 13 | * To create an option of a given type, from_string must be specialized for 14 | * parsing the type. 15 | * 16 | * @param string The string representation of the value. 17 | * @return The parsed value, if the string was valid. 18 | */ 19 | template 20 | std::optional from_string( 21 | const std::string& string); 22 | 23 | /** 24 | * To create an option of a given type, to_string must be specialized for 25 | * converting the type to string. 26 | * @return The string representation of a value. 27 | * It is expected that from_string(to_string(value)) == value. 28 | */ 29 | template 30 | std::string to_string(const Type& value); 31 | 32 | /** 33 | * Parse the given string as a signed 32-bit integer in decimal system. 34 | */ 35 | template<> 36 | std::optional from_string(const std::string&); 37 | 38 | /** 39 | * Parse the given string as a boolean value. 40 | * Truthy values are "True" (any capitalization) and 1. 41 | * False values are "False" (any capitalization) and 0. 42 | */ 43 | template<> 44 | std::optional from_string(const std::string&); 45 | 46 | /** 47 | * Parse the given string as a signed 64-bit floating point number. 48 | */ 49 | template<> 50 | std::optional from_string(const std::string&); 51 | 52 | /** 53 | * Parse the string as a string. 54 | * The string should not contain newline characters. 55 | */ 56 | template<> 57 | std::optional from_string(const std::string&); 58 | 59 | /** 60 | * Convert the given bool to a string. 61 | */ 62 | template<> 63 | std::string to_string(const bool& value); 64 | 65 | /** 66 | * Convert the given integer to a string. 67 | */ 68 | template<> 69 | std::string to_string(const int& value); 70 | 71 | /** 72 | * Convert the given double to a string. 73 | */ 74 | template<> 75 | std::string to_string(const double& value); 76 | 77 | /** 78 | * Convert the given string to a string. 79 | */ 80 | template<> 81 | std::string to_string(const std::string& value); 82 | } 83 | 84 | /** 85 | * Represents a color in RGBA format. 86 | */ 87 | struct color_t 88 | { 89 | public: 90 | /** Initialize a black transparent color (default) */ 91 | color_t(); 92 | 93 | /** 94 | * Initialize a new color value with the given values 95 | * Values will be clamped to the [0, 1] range. 96 | */ 97 | color_t(double r, double g, double b, double a); 98 | 99 | /** 100 | * Initialize a new color value with the given values. 101 | * Values will be clamped to the [0, 1] range. 102 | */ 103 | explicit color_t(const glm::vec4& value); 104 | 105 | /** 106 | * Compare colors channel-for-channel. 107 | * Comparisons use a small epsilon 1e-6. 108 | */ 109 | bool operator ==(const color_t& other) const; 110 | 111 | /** Red channel value */ 112 | double r; 113 | /** Green channel value */ 114 | double g; 115 | /** Blue channel value */ 116 | double b; 117 | /** Alpha channel value */ 118 | double a; 119 | }; 120 | 121 | namespace option_type 122 | { 123 | /** 124 | * Create a new color value from the given hex string, format is either 125 | * #RRGGBBAA or #RGBA. 126 | */ 127 | template<> 128 | std::optional from_string(const std::string& value); 129 | 130 | /** Convert the color to its hex string representation. */ 131 | template<> 132 | std::string to_string(const color_t& value); 133 | } 134 | 135 | /** 136 | * A list of valid modifiers. 137 | * The enumerations values are the same as the ones in wlroots. 138 | */ 139 | enum keyboard_modifier_t 140 | { 141 | /* Shift modifier, */ 142 | KEYBOARD_MODIFIER_SHIFT = 1, 143 | /* Control modifier, */ 144 | KEYBOARD_MODIFIER_CTRL = 4, 145 | /* Alt modifier, */ 146 | KEYBOARD_MODIFIER_ALT = 8, 147 | /* Windows/Mac logo modifier, */ 148 | KEYBOARD_MODIFIER_LOGO = 64, 149 | }; 150 | 151 | /** 152 | * Represents a single keyboard shortcut. 153 | */ 154 | struct keybinding_t 155 | { 156 | public: 157 | /** 158 | * Construct a new keybinding with the given modifier and key. 159 | */ 160 | keybinding_t(uint32_t modifier, uint32_t keyval); 161 | 162 | /* Check whether two keybindings refer to the same shortcut */ 163 | bool operator ==(const keybinding_t& other) const; 164 | 165 | /** @return The modifiers of the keybinding */ 166 | uint32_t get_modifiers() const; 167 | /** @return The key of the keybinding */ 168 | uint32_t get_key() const; 169 | 170 | private: 171 | /** The modifier mask of this keybinding */ 172 | uint32_t mod; 173 | /** The key of this keybinding */ 174 | uint32_t keyval; 175 | }; 176 | 177 | namespace option_type 178 | { 179 | /** 180 | * Construct a new keybinding from the given string description. 181 | * Format is .. KEY_, where whitespace 182 | * characters between the different modifiers and KEY_* are ignored. 183 | * 184 | * For a list of available modifieres, see @keyboard_modifier_t. 185 | * 186 | * The KEY_ is derived from evdev, and possible names are 187 | * enumerated in linux/input-event-codes.h 188 | * 189 | * For example, " KEY_E" represents pressing the Logo, Alt and 190 | * E keys together. 191 | * 192 | * Special cases are "none" and "disabled", which result in modifiers and 193 | * key 0. 194 | */ 195 | template<> 196 | std::optional from_string( 197 | const std::string& description); 198 | 199 | /** Represent the keybinding as a string. */ 200 | template<> 201 | std::string to_string(const keybinding_t& value); 202 | } 203 | 204 | /** 205 | * Represents a single button shortcut (pressing a mouse button while holding 206 | * modifiers). 207 | */ 208 | struct buttonbinding_t 209 | { 210 | public: 211 | /** 212 | * Construct a new buttonbinding with the given modifier and button. 213 | */ 214 | buttonbinding_t(uint32_t modifier, uint32_t button); 215 | 216 | /* Check whether two keybindings refer to the same shortcut */ 217 | bool operator ==(const buttonbinding_t& other) const; 218 | 219 | /** @return The modifiers of the buttonbinding */ 220 | uint32_t get_modifiers() const; 221 | /** @return The button of the buttonbinding */ 222 | uint32_t get_button() const; 223 | 224 | private: 225 | /** The modifier mask of this keybinding */ 226 | uint32_t mod; 227 | /** The key of this keybinding */ 228 | uint32_t button; 229 | }; 230 | 231 | namespace option_type 232 | { 233 | /** 234 | * Construct a new buttonbinding from the given description. 235 | * The format is the same as a keybinding, however instead of KEY_* values, 236 | * the buttons are prefixed with BTN_* 237 | * 238 | * Special case are descriptions "none" and "disable", which result in 239 | * mod = button = 0 240 | */ 241 | template<> 242 | std::optional from_string( 243 | const std::string& description); 244 | 245 | /** Represent the buttonbinding as a string. */ 246 | template<> 247 | std::string to_string(const buttonbinding_t& value); 248 | } 249 | 250 | /** 251 | * The different types of available gestures. 252 | */ 253 | enum touch_gesture_type_t 254 | { 255 | /* Invalid gesture */ 256 | GESTURE_TYPE_NONE = 0, 257 | /* Swipe gesture, i.e moving in one direction */ 258 | GESTURE_TYPE_SWIPE = 1, 259 | /* Edge swipe, which is a swipe originating from the edge of the screen */ 260 | GESTURE_TYPE_EDGE_SWIPE = 2, 261 | /* Pinch gesture, multiple touch points coming closer or farther apart 262 | * from the center */ 263 | GESTURE_TYPE_PINCH = 3, 264 | }; 265 | 266 | enum touch_gesture_direction_t 267 | { 268 | /* Swipe-specific */ 269 | GESTURE_DIRECTION_LEFT = (1 << 0), 270 | GESTURE_DIRECTION_RIGHT = (1 << 1), 271 | GESTURE_DIRECTION_UP = (1 << 2), 272 | GESTURE_DIRECTION_DOWN = (1 << 3), 273 | /* Pinch-specific */ 274 | GESTURE_DIRECTION_IN = (1 << 4), 275 | GESTURE_DIRECTION_OUT = (1 << 5), 276 | }; 277 | 278 | /** 279 | * Represents a touch gesture. 280 | * 281 | * A touch gesture has a type, direction and finger count. 282 | * Finger count can be arbitrary, although Wayfire supports only gestures 283 | * with finger count >= 3 currently. 284 | * 285 | * Direction can be either one of of @touch_gesture_direction_t or, in case of 286 | * the swipe gestures, it can be a bitwise OR of two non-opposing directions. 287 | */ 288 | struct touchgesture_t 289 | { 290 | /** 291 | * Construct a new touchgesture_t with the given type, direction and finger 292 | * count. Invalid combinations result in an invalid gesture with type NONE. 293 | */ 294 | touchgesture_t(touch_gesture_type_t type, uint32_t direction, 295 | int finger_count); 296 | 297 | /** @return The type of the gesture */ 298 | touch_gesture_type_t get_type() const; 299 | 300 | /** @return The finger count of the gesture, if valid. Undefined otherwise */ 301 | int get_finger_count() const; 302 | 303 | /** @return The direction of the gesture, if valid. Undefined otherwise */ 304 | uint32_t get_direction() const; 305 | 306 | /** 307 | * Check whether two bindings are equal. 308 | * Beware that a binding might be only partially set, i.e it might not have 309 | * a direction. In this case, the direction acts as a wildcard, so the 310 | * touchgesture_t matches any touchgesture_t of the same type with the same 311 | * finger count 312 | */ 313 | bool operator ==(const touchgesture_t& other) const; 314 | 315 | private: 316 | /** Type of the gesture */ 317 | touch_gesture_type_t type; 318 | /** Direction of the gesture */ 319 | uint32_t direction; 320 | /** Number of fingers of the gesture */ 321 | int finger_count; 322 | }; 323 | 324 | namespace option_type 325 | { 326 | /** 327 | * Construct a new touchgesture_t with the type, direction and finger count 328 | * indicated in the description. 329 | * 330 | * Format: 331 | * 1. pinch [in|out] 332 | * 2. [edge-]swipe up|down|left|right 333 | * 3. [edge-]swipe up-left|right-down|... 334 | * 4. disable | none 335 | */ 336 | template<> 337 | std::optional from_string( 338 | const std::string& description); 339 | 340 | /** Represent the touch gesture as a string. */ 341 | template<> 342 | std::string to_string(const touchgesture_t& value); 343 | } 344 | 345 | /** 346 | * The available edges of an output. 347 | */ 348 | enum output_edge_t 349 | { 350 | OUTPUT_EDGE_LEFT = (1 << 0), 351 | OUTPUT_EDGE_RIGHT = (1 << 1), 352 | OUTPUT_EDGE_TOP = (1 << 2), 353 | OUTPUT_EDGE_BOTTOM = (1 << 3), 354 | }; 355 | 356 | /** 357 | * Represents a binding which can be activated by moving the mouse into a 358 | * corner of the screen. 359 | */ 360 | struct hotspot_binding_t 361 | { 362 | /** 363 | * Initialize a hotspot with the given edges. 364 | * 365 | * @param edges The edges of the hotspot, a bitmask of output_edge_t 366 | * @param along_edge The size of the hotspot alongside the edge(s) 367 | * it is located on. 368 | * @param across_edge The size of the hotspot away from the edge(s) 369 | * it is located on. 370 | * @param timeout The time in milliseconds needed for the mouse to stay 371 | * in the hotspot to activate it. 372 | */ 373 | hotspot_binding_t(uint32_t edges = 0, int32_t along_edge = 0, 374 | int32_t away_from_edge = 0, int32_t timeout = 0); 375 | 376 | bool operator ==(const hotspot_binding_t& other) const; 377 | 378 | /** @return The edges this hotspot binding is on. */ 379 | uint32_t get_edges() const; 380 | 381 | /** @return The size along edges. */ 382 | int32_t get_size_along_edge() const; 383 | 384 | /** @return The size away from edges. */ 385 | int32_t get_size_away_from_edge() const; 386 | 387 | /** @return The timeout of the hotspot. */ 388 | int32_t get_timeout() const; 389 | 390 | private: 391 | uint32_t edges; 392 | int32_t along; 393 | int32_t away; 394 | int32_t timeout; 395 | }; 396 | 397 | namespace option_type 398 | { 399 | /** 400 | * Construct a new hotspot_binding_t with the specified edges and size 401 | * 402 | * Format: 403 | * hotspot top|...|top-left|... x 404 | */ 405 | template<> 406 | std::optional from_string( 407 | const std::string& description); 408 | 409 | /** Represent the hotspot binding as a string. */ 410 | template<> 411 | std::string to_string(const hotspot_binding_t& value); 412 | } 413 | 414 | /** 415 | * Represents a binding which can be activated via multiple actions - 416 | * keybindings, buttonbindings, touch gestures and hotspots. 417 | */ 418 | struct activatorbinding_t 419 | { 420 | public: 421 | /** 422 | * Initialize an empty activator binding, i.e one which cannot be activated 423 | * in any way. 424 | */ 425 | activatorbinding_t(); 426 | ~activatorbinding_t(); 427 | 428 | /* Copy constructor */ 429 | activatorbinding_t(const activatorbinding_t& other); 430 | /* Copy assignment */ 431 | activatorbinding_t& operator =(const activatorbinding_t& other); 432 | 433 | /** @return true if the activator is activated by the given keybinding. */ 434 | bool has_match(const keybinding_t& key) const; 435 | 436 | /** @return true if the activator is activated by the given buttonbinding. */ 437 | bool has_match(const buttonbinding_t& button) const; 438 | 439 | /** @return true if the activator is activated by the given gesture. */ 440 | bool has_match(const touchgesture_t& gesture) const; 441 | 442 | /** 443 | * @return A list of all hotspots which activate this binding. 444 | */ 445 | const std::vector& get_hotspots() const; 446 | 447 | /** 448 | * @return A list of all unknown bindings which activate this binding. 449 | */ 450 | const std::vector& get_extensions() const; 451 | 452 | /** 453 | * Check equality of two activator bindings. 454 | * 455 | * @return true if the two activator bindings are activated by the exact 456 | * same bindings, false otherwise. 457 | */ 458 | bool operator ==(const activatorbinding_t& other) const; 459 | 460 | public: 461 | struct impl; 462 | std::unique_ptr priv; 463 | }; 464 | 465 | namespace option_type 466 | { 467 | /** 468 | * Create an activator string from the given string description. 469 | * The string consists of valid descriptions of keybindings, buttonbindings 470 | * and touch gestures, separated by a single '|' sign. 471 | */ 472 | template<> 473 | std::optional from_string( 474 | const std::string& string); 475 | 476 | /** Represent the activator binding as a string. */ 477 | template<> 478 | std::string to_string(const activatorbinding_t& value); 479 | } 480 | 481 | /** 482 | * Types which are related to various output options. 483 | */ 484 | namespace output_config 485 | { 486 | enum mode_type_t 487 | { 488 | /** Output was configured in automatic mode. */ 489 | MODE_AUTO, 490 | /** Output was configured to be turned off. */ 491 | MODE_OFF, 492 | /** Output was configured with a given resolution. */ 493 | MODE_RESOLUTION, 494 | /** Output was configured to be a mirror of another output. */ 495 | MODE_MIRROR, 496 | }; 497 | 498 | /** 499 | * Represents the output mode. 500 | * It contains different values depending on the source. 501 | */ 502 | struct mode_t 503 | { 504 | /** 505 | * Initialize an OFF or AUTO mode. 506 | * 507 | * @param auto_on If true, the created mode will be an AUTO mode. 508 | */ 509 | mode_t(bool auto_on = false); 510 | 511 | /** 512 | * Initialize the mode with source self. 513 | * 514 | * @param width The configured width. 515 | * @param height The configured height. 516 | * @param refresh The configured refresh rate, or 0 if undefined. 517 | */ 518 | mode_t(int32_t width, int32_t height, int32_t refresh); 519 | 520 | /** 521 | * Initialize a mirror mode. 522 | */ 523 | mode_t(const std::string& mirror_from); 524 | 525 | /** @return The type of this mode. */ 526 | mode_type_t get_type() const; 527 | 528 | /** @return The configured width, if applicable. */ 529 | int32_t get_width() const; 530 | /** @return The configured height, if applicable. */ 531 | int32_t get_height() const; 532 | /** @return The configured refresh rate, if applicable. */ 533 | int32_t get_refresh() const; 534 | 535 | /** @return The configured mirror from output, if applicable. */ 536 | std::string get_mirror_from() const; 537 | 538 | /** 539 | * Check equality of two modes. 540 | * 541 | * @return true if the modes have the same source types and parameters. 542 | */ 543 | bool operator ==(const mode_t& other) const; 544 | 545 | private: 546 | int32_t width; 547 | int32_t height; 548 | int32_t refresh; 549 | 550 | std::string mirror_from; 551 | 552 | mode_type_t type; 553 | }; 554 | 555 | /** 556 | * Represents the output's position. 557 | */ 558 | struct position_t 559 | { 560 | /** Automatically positioned output. */ 561 | position_t(); 562 | 563 | /** Output positioned at a fixed position. */ 564 | position_t(int32_t x, int32_t y); 565 | 566 | /** @return The configured X coordinate. */ 567 | int32_t get_x() const; 568 | /** @return The configured X coordinate. */ 569 | int32_t get_y() const; 570 | 571 | /** @return whether the output is automatically positioned. */ 572 | bool is_automatic_position() const; 573 | 574 | bool operator ==(const position_t& other) const; 575 | 576 | private: 577 | int32_t x; 578 | int32_t y; 579 | bool automatic; 580 | }; 581 | } 582 | 583 | namespace option_type 584 | { 585 | /** 586 | * Create a mode from its string description. 587 | * The supported formats are: 588 | * 589 | * For MODE_AUTO: auto|default 590 | * For MODE_OFF: off 591 | * For MODE_RESOLUTION: WxH[@RR] 592 | * For MODE_MIRROR: mirror 593 | */ 594 | template<> 595 | std::optional from_string( 596 | const std::string& string); 597 | 598 | /** Represent the activator binding as a string. */ 599 | template<> 600 | std::string to_string(const output_config::mode_t& value); 601 | 602 | /** 603 | * Create an output position from its string description. 604 | * The supported formats are: 605 | * 606 | * auto|default 607 | * x , y 608 | */ 609 | template<> 610 | std::optional from_string( 611 | const std::string& string); 612 | 613 | /** Represent the activator binding as a string. */ 614 | template<> 615 | std::string to_string(const output_config::position_t& value); 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /src/file.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "option-impl.hpp" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | class line_t : public std::string 20 | { 21 | public: 22 | template 23 | line_t(T source) : std::string(source) 24 | {} 25 | 26 | line_t() : std::string() 27 | {} 28 | line_t(const line_t& other) = default; 29 | line_t(line_t&& other) = default; 30 | line_t& operator =(const line_t& other) = default; 31 | line_t& operator =(line_t&& other) = default; 32 | 33 | public: 34 | line_t substr(size_t start, size_t length = npos) const 35 | { 36 | line_t result = std::string::substr(start, length); 37 | result.source_line_number = this->source_line_number; 38 | return result; 39 | } 40 | 41 | size_t source_line_number; 42 | }; 43 | 44 | using lines_t = std::vector; 45 | 46 | static lines_t split_to_lines(const std::string& source) 47 | { 48 | std::istringstream stream(source); 49 | lines_t output; 50 | line_t line; 51 | 52 | size_t line_idx = 1; 53 | while (std::getline(stream, line)) 54 | { 55 | line.source_line_number = line_idx; 56 | output.push_back(line); 57 | ++line_idx; 58 | } 59 | 60 | return output; 61 | } 62 | 63 | /** 64 | * Check whether at the given index @idx in @line, there is a character 65 | * @ch which isn't escaped (i.e preceded by \). 66 | */ 67 | static bool is_nonescaped(const std::string& line, char ch, int idx) 68 | { 69 | return line[idx] == ch && (idx == 0 || line[idx - 1] != '\\'); 70 | } 71 | 72 | static size_t find_first_nonescaped(const std::string& line, char ch) 73 | { 74 | /* Find first not-escaped # */ 75 | size_t pos = 0; 76 | while (pos != std::string::npos && !is_nonescaped(line, ch, pos)) 77 | { 78 | pos = line.find(ch, pos + 1); 79 | } 80 | 81 | return pos; 82 | } 83 | 84 | line_t remove_escaped_sharps(const line_t& line) 85 | { 86 | line_t result; 87 | result.source_line_number = line.source_line_number; 88 | 89 | bool had_escape = false; 90 | for (auto& ch : line) 91 | { 92 | if ((ch == '#') && had_escape) 93 | { 94 | result.pop_back(); 95 | } 96 | 97 | result += ch; 98 | had_escape = (ch == '\\'); 99 | } 100 | 101 | return result; 102 | } 103 | 104 | static lines_t remove_comments(const lines_t& lines) 105 | { 106 | lines_t result; 107 | for (const auto& line : lines) 108 | { 109 | auto pos = find_first_nonescaped(line, '#'); 110 | result.push_back( 111 | remove_escaped_sharps(line.substr(0, pos))); 112 | } 113 | 114 | return result; 115 | } 116 | 117 | static lines_t remove_trailing_whitespace(const lines_t& lines) 118 | { 119 | lines_t result; 120 | for (const auto& line : lines) 121 | { 122 | auto result_line = line; 123 | while (!result_line.empty() && std::isspace(result_line.back())) 124 | { 125 | result_line.pop_back(); 126 | } 127 | 128 | result.push_back(result_line); 129 | } 130 | 131 | return result; 132 | } 133 | 134 | lines_t join_lines(const lines_t& lines) 135 | { 136 | lines_t result; 137 | bool in_concat_mode = false; 138 | 139 | for (const auto& line : lines) 140 | { 141 | if (in_concat_mode) 142 | { 143 | assert(!result.empty()); 144 | result.back() += line; 145 | } else 146 | { 147 | result.push_back(line); 148 | } 149 | 150 | if (result.empty() || result.back().empty()) 151 | { 152 | in_concat_mode = false; 153 | } else 154 | { 155 | in_concat_mode = (result.back().back() == '\\'); 156 | if (in_concat_mode) /* pop last \ */ 157 | { 158 | result.back().pop_back(); 159 | } 160 | 161 | /* If last \ was escaped, we should ignore it */ 162 | bool was_escaped = 163 | !result.back().empty() && result.back().back() == '\\'; 164 | in_concat_mode = in_concat_mode && !was_escaped; 165 | } 166 | } 167 | 168 | return result; 169 | } 170 | 171 | lines_t skip_empty(const lines_t& lines) 172 | { 173 | lines_t result; 174 | for (auto& line : lines) 175 | { 176 | if (!line.empty()) 177 | { 178 | result.push_back(line); 179 | } 180 | } 181 | 182 | return result; 183 | } 184 | 185 | static std::string ignore_leading_trailing_whitespace(const std::string& string) 186 | { 187 | if (string.empty()) 188 | { 189 | return ""; 190 | } 191 | 192 | size_t i = 0; 193 | size_t j = string.size() - 1; 194 | while (i < j && std::isspace(string[i])) 195 | { 196 | ++i; 197 | } 198 | 199 | while (i < j && std::isspace(string[j])) 200 | { 201 | --j; 202 | } 203 | 204 | return string.substr(i, j - i + 1); 205 | } 206 | 207 | enum option_parsing_result 208 | { 209 | /* Line was valid */ 210 | OPTION_PARSED_OK, 211 | /* Line has wrong format */ 212 | OPTION_PARSED_WRONG_FORMAT, 213 | /* Specified value does not match existing option type */ 214 | OPTION_PARSED_INVALID_CONTENTS, 215 | }; 216 | 217 | /** 218 | * Try to parse an option line. If the option line is valid, the corresponding option is modified or added to 219 | * @current_section, and the option is added to @reloaded. 220 | * 221 | * @return The parse status of the line. 222 | */ 223 | static option_parsing_result parse_option_line( 224 | wf::config::section_t& current_section, const line_t& line, 225 | std::set>& reloaded) 226 | { 227 | size_t equal_sign = line.find_first_of("="); 228 | if (equal_sign == std::string::npos) 229 | { 230 | return OPTION_PARSED_WRONG_FORMAT; 231 | } 232 | 233 | auto name = ignore_leading_trailing_whitespace(line.substr(0, equal_sign)); 234 | auto value = ignore_leading_trailing_whitespace(line.substr(equal_sign + 1)); 235 | 236 | auto option = current_section.get_option_or(name); 237 | if (!option) 238 | { 239 | using namespace wf; 240 | option = std::make_shared>(name, ""); 241 | option->set_value_str(value); 242 | current_section.register_new_option(option); 243 | } 244 | 245 | if (option->is_locked() || option->set_value_str(value)) 246 | { 247 | reloaded.insert(option); 248 | return OPTION_PARSED_OK; 249 | } 250 | 251 | return OPTION_PARSED_INVALID_CONTENTS; 252 | } 253 | 254 | /** 255 | * Check whether the @line is a valid section start. If yes, it will either return the section in @config with 256 | * the same name, or create a new section and register it in config. 257 | * 258 | * @return nullptr if line is not a valid section, the section otherwise. 259 | */ 260 | static std::shared_ptr check_section( 261 | wf::config::config_manager_t& config, const line_t& line) 262 | { 263 | auto name = ignore_leading_trailing_whitespace(line); 264 | if (name.empty() || (name.front() != '[') || (name.back() != ']')) 265 | { 266 | return {}; 267 | } 268 | 269 | auto real_name = name.substr(1, name.length() - 2); 270 | 271 | auto section = config.get_section(real_name); 272 | if (!section) 273 | { 274 | size_t splitter = real_name.find_first_of(":"); 275 | if (splitter != std::string::npos) 276 | { 277 | auto obj_type_name = real_name.substr(0, splitter); 278 | auto section_name = real_name.substr(splitter + 1); // only for the 279 | // empty check 280 | if (!obj_type_name.empty() && !section_name.empty()) 281 | { 282 | auto parent_section = config.get_section(obj_type_name); 283 | if (parent_section) 284 | { 285 | section = parent_section->clone_with_name(real_name); 286 | config.merge_section(section); 287 | return section; 288 | } 289 | } 290 | } 291 | 292 | section = std::make_shared(real_name); 293 | config.merge_section(section); 294 | } 295 | 296 | return section; 297 | } 298 | 299 | void wf::config::load_configuration_options_from_string( 300 | config_manager_t& config, const std::string& source, 301 | const std::string& source_name) 302 | { 303 | std::set> reloaded; 304 | 305 | auto lines = 306 | skip_empty( 307 | join_lines( 308 | remove_trailing_whitespace( 309 | remove_comments( 310 | split_to_lines(source))))); 311 | 312 | std::shared_ptr current_section; 313 | 314 | for (const auto& line : lines) 315 | { 316 | auto next_section = check_section(config, line); 317 | if (next_section) 318 | { 319 | current_section = next_section; 320 | continue; 321 | } 322 | 323 | if (!current_section) 324 | { 325 | LOGE("Error in file ", source_name, ":", line.source_line_number, 326 | ", option declared before a section starts!"); 327 | continue; 328 | } 329 | 330 | auto status = parse_option_line(*current_section, line, reloaded); 331 | switch (status) 332 | { 333 | case OPTION_PARSED_WRONG_FORMAT: 334 | LOGE("Error in file ", source_name, ":", 335 | line.source_line_number, ", invalid option format ", 336 | "(allowed = )"); 337 | break; 338 | 339 | case OPTION_PARSED_INVALID_CONTENTS: 340 | LOGE("Error in file ", source_name, ":", 341 | line.source_line_number, ", invalid option value!"); 342 | break; 343 | 344 | default: 345 | break; 346 | } 347 | } 348 | 349 | // Go through all options and reset options which are loaded from the config 350 | // string but are not there anymore. 351 | for (auto section : config.get_all_sections()) 352 | { 353 | for (auto opt : section->get_registered_options()) 354 | { 355 | opt->priv->option_in_config_file = (reloaded.count(opt) > 0); 356 | 357 | opt->priv->is_part_compound = false; // will be re-set when updating compound options 358 | opt->priv->could_be_compound = false; // will be re-set when updating compound options 359 | 360 | if (!opt->priv->option_in_config_file && !opt->is_locked()) 361 | { 362 | opt->reset_to_default(); 363 | } 364 | } 365 | } 366 | 367 | // After resetting all options which are no longer in the config file, make 368 | // sure to rebuild compound options as well. 369 | for (auto section : config.get_all_sections()) 370 | { 371 | for (auto opt : section->get_registered_options()) 372 | { 373 | auto as_compound = std::dynamic_pointer_cast(opt); 374 | if (as_compound) 375 | { 376 | update_compound_from_section(*as_compound, section); 377 | } 378 | } 379 | } 380 | 381 | for (auto section : config.get_all_sections()) 382 | { 383 | for (auto opt : section->get_registered_options()) 384 | { 385 | if (!opt->priv->xml && !opt->priv->is_part_compound) 386 | { 387 | if (opt->priv->could_be_compound) 388 | { 389 | LOGW("Option ", section->get_name(), "/", opt->get_name(), 390 | " could not be parsed as part of a compound option: missing entries or wrong type!"); 391 | } else 392 | { 393 | LOGW("Loaded option ", section->get_name(), "/", opt->get_name(), 394 | ", which does not belong to any registered plugin, nor could be parsed as a part of ", 395 | "a compound list option. Make sure all the relevant XML files are installed and " 396 | "that the option name is spelled correctly!"); 397 | } 398 | } 399 | } 400 | } 401 | } 402 | 403 | std::string wf::config::save_configuration_options_to_string( 404 | const config_manager_t& config) 405 | { 406 | std::vector lines; 407 | 408 | for (auto& section : config.get_all_sections()) 409 | { 410 | lines.push_back("[" + section->get_name() + "]"); 411 | 412 | // Go through each option and add the necessary lines. 413 | // Take care so that regular options overwrite compound options 414 | // in case of conflict! 415 | std::map option_values; 416 | std::set all_compound_prefixes; 417 | for (auto& option : section->get_registered_options()) 418 | { 419 | auto as_compound = std::dynamic_pointer_cast(option); 420 | if (as_compound) 421 | { 422 | auto value = as_compound->get_value_untyped(); 423 | const auto& prefixes = as_compound->get_entries(); 424 | for (auto& p : prefixes) 425 | { 426 | all_compound_prefixes.insert(p->get_prefix()); 427 | } 428 | 429 | for (size_t i = 0; i < value.size(); i++) 430 | { 431 | for (size_t j = 0; j < prefixes.size(); j++) 432 | { 433 | auto full_name = prefixes[j]->get_prefix() + value[i][0]; 434 | option_values[full_name] = value[i][j + 1]; 435 | } 436 | } 437 | } 438 | } 439 | 440 | // An option is part of a compound option if it begins with any of the 441 | // prefixes. 442 | const auto& is_part_of_compound_option = [&] (const std::string& name) 443 | { 444 | return std::any_of( 445 | all_compound_prefixes.begin(), all_compound_prefixes.end(), 446 | [&] (const auto& prefix) 447 | { 448 | return name.substr(0, prefix.size()) == prefix; 449 | }); 450 | }; 451 | 452 | for (auto& option : section->get_registered_options()) 453 | { 454 | auto as_compound = std::dynamic_pointer_cast(option); 455 | if (!as_compound) 456 | { 457 | // Check whether this option does not conflict with a compound 458 | // option entry. 459 | if (xml::get_option_xml_node(option) || 460 | !is_part_of_compound_option(option->get_name())) 461 | { 462 | option_values[option->get_name()] = option->get_value_str(); 463 | } 464 | } 465 | } 466 | 467 | for (auto& [name, value] : option_values) 468 | { 469 | lines.push_back(name + " = " + value); 470 | } 471 | 472 | lines.push_back(""); 473 | } 474 | 475 | /* Check which characters need escaping */ 476 | for (auto& line : lines) 477 | { 478 | size_t sharp = line.find_first_of("#"); 479 | while (sharp != line.npos) 480 | { 481 | line.insert(line.begin() + sharp, '\\'); 482 | sharp = line.find_first_of("#", sharp + 2); 483 | } 484 | 485 | if (!line.empty() && (line.back() == '\\')) 486 | { 487 | line += '\\'; 488 | } 489 | } 490 | 491 | std::string result; 492 | for (const auto& line : lines) 493 | { 494 | result += line + "\n"; 495 | } 496 | 497 | return result; 498 | } 499 | 500 | static std::string load_file_contents(const std::string& file) 501 | { 502 | std::ifstream infile(file); 503 | std::string file_contents((std::istreambuf_iterator(infile)), 504 | std::istreambuf_iterator()); 505 | 506 | return file_contents; 507 | } 508 | 509 | bool wf::config::load_configuration_options_from_file(config_manager_t& manager, 510 | const std::string& file) 511 | { 512 | /* Try to lock the file */ 513 | auto fd = open(file.c_str(), O_RDONLY); 514 | if (flock(fd, LOCK_SH | LOCK_NB)) 515 | { 516 | close(fd); 517 | return false; 518 | } 519 | 520 | auto file_contents = load_file_contents(file); 521 | 522 | /* Release lock */ 523 | flock(fd, LOCK_UN); 524 | close(fd); 525 | 526 | load_configuration_options_from_string(manager, file_contents, file); 527 | return true; 528 | } 529 | 530 | void wf::config::save_configuration_to_file( 531 | const wf::config::config_manager_t& manager, const std::string& file) 532 | { 533 | auto contents = save_configuration_options_to_string(manager); 534 | contents.pop_back(); // remove last newline 535 | 536 | auto fd = open(file.c_str(), O_RDONLY); 537 | flock(fd, LOCK_EX); 538 | 539 | auto fout = std::ofstream(file, std::ios::trunc); 540 | fout << contents; 541 | 542 | flock(fd, LOCK_UN); 543 | close(fd); 544 | 545 | /* Modify the file one last time. Now programs waiting for updates can acquire a shared lock. */ 546 | fout << std::endl; 547 | } 548 | 549 | static void process_xml_file(wf::config::config_manager_t& manager, 550 | const std::string & filename) 551 | { 552 | /* Parse the XML file. */ 553 | auto doc = xmlParseFile(filename.c_str()); 554 | if (!doc) 555 | { 556 | LOGE("Failed to parse XML file ", filename); 557 | return; 558 | } 559 | 560 | auto root = xmlDocGetRootElement(doc); 561 | if (!root) 562 | { 563 | LOGE(filename, ": missing root element."); 564 | xmlFreeDoc(doc); 565 | return; 566 | } 567 | 568 | /* Seek the plugin/object sections */ 569 | auto section = root->children; 570 | while (section != nullptr) 571 | { 572 | if ((section->type == XML_ELEMENT_NODE) && 573 | (((const char*)section->name == (std::string)"plugin") || 574 | ((const char*)section->name == (std::string)"object"))) 575 | { 576 | manager.merge_section( 577 | wf::config::xml::create_section_from_xml_node(section)); 578 | } 579 | 580 | section = section->next; 581 | } 582 | 583 | // xmlFreeDoc(doc); - May clear the XML nodes before they are used 584 | } 585 | 586 | static wf::config::config_manager_t load_xml_files(const std::vector& xmldirs) 587 | { 588 | wf::config::config_manager_t manager; 589 | 590 | for (auto& xmldir : xmldirs) 591 | { 592 | auto xmld = opendir(xmldir.c_str()); 593 | if (!xmld) 594 | { 595 | LOGW("Failed to open XML directory ", xmldir); 596 | continue; 597 | } 598 | 599 | std::vector loaded_files; 600 | 601 | struct dirent *entry; 602 | while ((entry = readdir(xmld)) != nullptr) 603 | { 604 | if ((entry->d_type != DT_LNK) && (entry->d_type != DT_REG) && 605 | (entry->d_type != DT_UNKNOWN)) 606 | { 607 | continue; 608 | } 609 | 610 | std::string filename = xmldir + '/' + entry->d_name; 611 | if ((filename.length() > 4) && 612 | (filename.rfind(".xml") == filename.length() - 4)) 613 | { 614 | process_xml_file(manager, filename); 615 | loaded_files.push_back(entry->d_name); 616 | } 617 | } 618 | 619 | closedir(xmld); 620 | 621 | if (!loaded_files.empty()) 622 | { 623 | LOGI("Loaded XML configuration options from ", loaded_files.size(), 624 | " files in ", xmldir, ":"); 625 | 626 | std::string list; 627 | for (size_t i = 0; i < loaded_files.size(); ++i) 628 | { 629 | list += loaded_files[i]; 630 | if (i + 1 != loaded_files.size()) 631 | { 632 | list += ", "; 633 | } 634 | } 635 | 636 | LOGI(list); 637 | } 638 | } 639 | 640 | return manager; 641 | } 642 | 643 | void override_defaults(wf::config::config_manager_t& manager, 644 | const std::string& sysconf) 645 | { 646 | auto sysconf_str = load_file_contents(sysconf); 647 | 648 | wf::config::config_manager_t overrides; 649 | load_configuration_options_from_string(overrides, sysconf_str, sysconf); 650 | for (auto& section : overrides.get_all_sections()) 651 | { 652 | for (auto& option : section->get_registered_options()) 653 | { 654 | auto full_name = section->get_name() + '/' + option->get_name(); 655 | auto real_option = manager.get_option(full_name); 656 | if (real_option) 657 | { 658 | if (!real_option->set_default_value_str( 659 | option->get_value_str())) 660 | { 661 | LOGW("Invalid value for ", full_name, " in ", sysconf); 662 | } else 663 | { 664 | /* Set the value to the new default */ 665 | real_option->reset_to_default(); 666 | } 667 | } else 668 | { 669 | LOGW("Unused default value for ", full_name, " in ", sysconf); 670 | } 671 | } 672 | } 673 | } 674 | 675 | #include 676 | 677 | wf::config::config_manager_t wf::config::build_configuration( 678 | const std::vector& xmldirs, const std::string& sysconf, 679 | const std::string& userconf) 680 | { 681 | auto manager = load_xml_files(xmldirs); 682 | override_defaults(manager, sysconf); 683 | load_configuration_options_from_file(manager, userconf); 684 | return manager; 685 | } 686 | --------------------------------------------------------------------------------