diff --git a/.gitignore b/.gitignore index 2e08a78..77fb953 100644 --- a/.gitignore +++ b/.gitignore @@ -103,8 +103,6 @@ dkms.conf cmake-** .idea -.fleet -.cache .vs .vscode build \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e736a4..3e27952 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,180 +1,56 @@ -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.20) + project(cigui - VERSION 0.0.1 - LANGUAGES CXX) - -# Options -option(CIGUI_BUILD_SHARED "Build CIGUI as a shared library" ON) -option(CIGUI_BUILD_EXAMPLES "Build example applications" ON) -option(CIGUI_BUILD_TESTS "Build tests" OFF) -option(CIGUI_ENABLE_ASAN "Enable Address Sanitizer (Debug)" OFF) -option(CIGUI_ENABLE_UBSAN "Enable Undefined Behavior Sanitizer (Debug)" OFF) -option(CIGUI_ENABLE_WARNINGS "Enable additional compiler warnings" ON) + VERSION 0.0.1 + LANGUAGES CXX) -# Set C++ standard set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Set debug postfix -set(CMAKE_DEBUG_POSTFIX "-d") -# Configure debug symbols and warnings -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - if(MSVC) - add_compile_options(/Zi) # Debug symbols for MSVC - if(CIGUI_ENABLE_WARNINGS) - add_compile_options(/W4) # Higher warning level - endif() - else() - add_compile_options(-g) # Debug symbols for GCC/Clang - if(CIGUI_ENABLE_WARNINGS) - add_compile_options( - -Wall - -Wextra - -Wpedantic - -Wshadow - -Wconversion - ) - endif() - - # Enable sanitizers in debug builds if requested - if(CIGUI_ENABLE_ASAN) - add_compile_options(-fsanitize=address -fno-omit-frame-pointer) - add_link_options(-fsanitize=address) - endif() - - if(CIGUI_ENABLE_UBSAN) - add_compile_options(-fsanitize=undefined) - add_link_options(-fsanitize=undefined) - endif() - endif() -endif() - -# Define export macros -if(CIGUI_BUILD_SHARED) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) - if(WIN32) - add_definitions(-DCIGUI_DLL) - endif() -endif() - -# Include CPM.cmake for dependency management include(cmake/CPM.cmake) - -# Add SFML dependency using CPM -CPMAddPackage( - NAME SFML - GITHUB_REPOSITORY SFML/SFML - GIT_TAG 3.0.0 # Adjust to actual SFML 3 version/tag - OPTIONS - "SFML_BUILD_AUDIO OFF" - "SFML_BUILD_NETWORK OFF" - "CMAKE_DEBUG_POSTFIX -d" # Add debug postfix for debug builds -) - -# Generate export macros -include(GenerateExportHeader) -configure_file( - ${CMAKE_CURRENT_SOURCE_DIR}/include/cigui/config.h.in - ${CMAKE_CURRENT_BINARY_DIR}/include/cigui/config.h -) - -# Define library target -if(CIGUI_BUILD_SHARED) - add_library(cigui SHARED) -else() - add_library(cigui STATIC) -endif() - -# Enable precompiled headers for faster builds -if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.16) - target_precompile_headers(cigui PRIVATE - - - - - - - ) -endif() - -# Add source files include(cmake/utils.cmake) -find_files(CIGUI_SOURCES "src" cpp c cxx) -find_files(CIGUI_HEADERS "src" hpp h inl hxx) -target_sources(cigui PRIVATE ${CIGUI_SOURCES}) -# Configure include directories -target_include_directories(cigui - PUBLIC - $ - $ - $ - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src +CPMAddPackage( + NAME spdlog + GITHUB_REPOSITORY gabime/spdlog + VERSION 1.15.2 + GIT_SHALLOW ON + EXCLUDE_FROM_ALL ) -# Link with SFML -target_link_libraries(cigui PUBLIC sfml-graphics sfml-window sfml-system) - -# Define installation -include(GNUInstallDirs) -include(CMakePackageConfigHelpers) - -# Install headers -install( - DIRECTORY include/cigui/ - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cigui - FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h" -) -install( - FILES ${CMAKE_CURRENT_BINARY_DIR}/include/cigui/config.h - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cigui +CPMAddPackage( + NAME sfml + GITHUB_REPOSITORY SFML/SFML + GIT_TAG 3.0.0 + GIT_SHALLOW ON + EXCLUDE_FROM_ALL + SYSTEM ) -# Install libraries -install( - TARGETS cigui - EXPORT cigui-targets - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +CPMAddPackage( + NAME nlohmann_json + GITHUB_REPOSITORY nlohmann/json + VERSION 3.11.2 + GIT_SHALLOW ON + EXCLUDE_FROM_ALL ) -# Create and install config files -configure_package_config_file( - cmake/CIGUIConfig.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/CIGUIConfig.cmake - INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cigui -) -write_basic_package_version_file( - ${CMAKE_CURRENT_BINARY_DIR}/CIGUIConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion -) -install( - FILES - ${CMAKE_CURRENT_BINARY_DIR}/CIGUIConfig.cmake - ${CMAKE_CURRENT_BINARY_DIR}/CIGUIConfigVersion.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cigui -) -install( - EXPORT cigui-targets - FILE CIGUITargets.cmake - NAMESPACE cig:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cigui -) +set(PROJECT_SOURCE_NAME "${PROJECT_NAME}_SOURCES") -# Add examples directory -if(CIGUI_BUILD_EXAMPLES) - add_subdirectory(examples) -endif() +find_files(ExampleSources src cpp hpp c h cxx hxx) -# Add tests directory -if(CIGUI_BUILD_TESTS) - include(CTest) - add_subdirectory(tests) -endif() +add_executable(${PROJECT_NAME} ${ExampleSources} cigui.hpp) + +target_include_directories(${PROJECT_NAME} PUBLIC include ./) + +target_link_libraries(${PROJECT_NAME} PUBLIC + spdlog::spdlog + SFML::Graphics + SFML::Window + SFML::System + SFML::Audio + SFML::Network + nlohmann_json::nlohmann_json) \ No newline at end of file diff --git a/README.md b/README.md index 412e09e..b1910e7 100644 --- a/README.md +++ b/README.md @@ -1,251 +1,6 @@ # cigui -## CIGUI Style Guide - -This document outlines the coding standards and best practices for the CIGUI library. Following these guidelines ensures consistency across the project and makes the codebase more maintainable. - -### General Principles - -- **Clarity over cleverness**: Write code that's easy to understand, not code that's clever. -- **Consistency**: Follow established patterns within the codebase. -- **Documentation**: Document public APIs thoroughly. -- **Testability**: Write code that can be easily tested. - -### Naming Conventions - -#### Files - -- Header files: `.hpp` for C++ headers, `.h` for C-compatible headers -- Implementation files: `.cpp` -- Template implementation files: `.inl` -- File names: PascalCase (e.g., `GridLayout.hpp`) - -#### Classes and Types - -- Class names: PascalCase (e.g., `Button`, `GridLayout`) -- Type aliases/typedefs: lowercase (e.g., `using view_ptr = std::shared_ptr`) -- Enum names: PascalCase -- Enum values: PascalCase/UPPERCASE (e.g., `enum class Alignment { TopLeft, Center, BottomRight }` or `enum class Direction { LEFT, UP, RIGHT, DOWN }`) - -#### Functions and Variables - -- API function names: camelCase (e.g., `getPosition()`, `setVisible()`) -- Internal function names: lowercase (e.g. `render_view()`, `get_visibility()`) -- Variable names: camelCase (e.g., `buttonText`, `isVisible`) -- Member variables: prefix with `m_` + UpperCamelCase (e.g., `m_Position`, `m_Size`) -- Static variables: prefix with `s_` + UpperCamelCase (e.g., `s_DefaultFont`) -- Constants and macros: ALL_CAPS with underscores (e.g., `MAX_WIDGETS`, `CIGUI_API`) - -### Code Structure - -#### Namespaces - -- All library code should be inside the `cig` namespace -- Avoid deeply nested namespaces -- Do not use `using namespace` in headers - -```cpp -// Good -namespace cig { - class Button : public View { - // ... - }; -} - -// Bad -using namespace sf; // Don't do this in headers -``` - -#### Headers - -- Always use include guards with project-specific prefix -- Order includes as follows: - 1. Related header - 2. C++ standard library headers - 3. Third-party library headers (SFML) - 4. CIGUI headers -- Forward declare classes when possible to reduce dependencies - -```cpp -// Example of a good header structure -#pragma once - -#include -#include -#include -#include -#include - -namespace cig { - -class View; // Forward declaration - -class CIGUI_API Container { - // Implementation -}; - -} // namespace cig -``` - -#### Classes - -- Separate public, protected, and private sections -- Order within sections: - 1. Constructors/Destructors - 2. Public methods - 3. Event callbacks - 4. Static methods - 5. Member variables - -```cpp -class Button : public View { -public: - // Constructors/Destructors - Button(); - explicit Button(const std::string& text); - ~Button() override; - - // Methods - void setText(const std::string& text); - const std::string& getText() const; - - // Event dispatcher - EventDispatcher onClicked; - -protected: - // Protected methods - void updateAppearance(); - -private: - // Private methods - void initializeGraphics(); - - // Member variables - std::string m_text; - sf::RectangleShape m_background; - bool m_isHovered; -}; -``` - -### Templates - -- Template implementation should be in `.inl` files -- Use explicit instantiation for common types in DLLs -- Document template parameters - -```cpp -// In .hpp file -template -class Container { - // ... -}; - -// Include the implementation -#include - -// In .inl file -template -Container::Container() { - // ... -} - -// In .cpp file for explicit instantiation -template class Container; -template class Container; -``` - -### C++ Features - -- **C++ Standard**: Use C++20 features where appropriate -- **Smart Pointers**: Use `std::unique_ptr` for exclusive ownership, `std::shared_ptr` for shared ownership -- **Auto**: Use `auto` when the type is obvious or when using iterators -- **Range-based for loops**: Prefer over traditional for loops -- **Lambdas**: Use for short callbacks and event handlers -- **Move Semantics**: Support move operations where appropriate - -### Comments and Documentation - -- Use Doxygen-style comments for public APIs -- Comment complex algorithms and non-obvious code -- Avoid redundant comments that just repeat the code - -```cpp -/** - * @brief Creates a button with the specified label text - * - * @param text The text to display on the button - * @param size The size of the button (default: 100x30) - */ -Button(const std::string& text, const sf::Vector2f& size = {100.f, 30.f}); -``` - -### Error Handling - -- Use exceptions for exceptional cases only -- Validate input parameters and handle edge cases -- Document error conditions in function comments - -### Memory Management - -- Prefer automatic memory management with smart pointers -- Explicitly define ownership models in documentation -- Design with RAII principles (Resource Acquisition Is Initialization) - -### DLL/Shared Library Considerations - -- Use `CIGUI_API` macro for all classes and non-inline functions -- Use `CIGUI_TEMPLATE_API` for template classes -- Handle template instantiation properly (see Templates section) - -### SFML Integration - -- Wrap SFML types when extending functionality -- Use SFML conventions for graphics-related code -- Don't expose SFML implementation details in public APIs when avoidable - -### Testing - -- Write unit tests for core functionality -- Test edge cases and error conditions -- Create interactive examples for UI components - -### Formatting - -- Indentation: 4 spaces (no tabs) -- Line length: 100 characters maximum -- Braces: Open brace on same line, close brace on new line -- Space after keywords (if, for, while) -- No space after function names -- Place * and & with the type, not the variable name - -```cpp -// Good formatting example -if (condition) { - doSomething(); -} else { - doSomethingElse(); -} - -void setPosition(const sf::Vector2f& position) { - m_position = position; -} -``` - -### Best Practices - -- Prefer composition over inheritance -- Design interfaces that are hard to use incorrectly -- Follow the Rule of Five/Zero for class design -- Make data members private and provide accessors when needed -- Consider performance implications in UI code (avoid work in draw methods) - ---- - -This style guide is a living document and may evolve as the project grows. When in doubt, maintain consistency with the existing codebase. - ---- - -## Goal +# Goal ```cpp #include diff --git a/cigui.hpp b/cigui.hpp new file mode 100644 index 0000000..b48bdcd --- /dev/null +++ b/cigui.hpp @@ -0,0 +1,291 @@ +#pragma once + +#include +#include +#include +#include + + +#define TYPEDEF_VECTOR(NAME, T, N, s) typedef NAME vec##N##s; + +#define TYPEDEF_VECTORS(NAME, N) \ + typedef NAME vec##N; \ + TYPEDEF_VECTOR(NAME, float, N, f) \ + TYPEDEF_VECTOR(NAME, double, N, d) \ + TYPEDEF_VECTOR(NAME, long double, N, ld) \ + TYPEDEF_VECTOR(NAME, size_t, N, sz) \ + TYPEDEF_VECTOR(NAME, int, N, i) \ + TYPEDEF_VECTOR(NAME, unsigned int, N, u) \ + TYPEDEF_VECTOR(NAME, short, N, s) \ + TYPEDEF_VECTOR(NAME, unsigned short, N, us) \ + TYPEDEF_VECTOR(NAME, long, N, l) \ + TYPEDEF_VECTOR(NAME, unsigned long, N, ul) \ + TYPEDEF_VECTOR(NAME, long long, N, ll) \ + TYPEDEF_VECTOR(NAME, unsigned long long, N, ull) + + + + +namespace cig +{ + namespace utils + { + template + class List + { + std::unique_ptr m_Data; + size_t m_Size = 0; + size_t m_Capacity; + + protected: + void reserve(const size_t capacity) + { + if (!m_Data) + { + m_Data = std::unique_ptr(static_cast(calloc(capacity, sizeof(T)))); + m_Capacity = capacity; + return; + } + std::unique_ptr newData(static_cast(calloc(capacity, sizeof(T)))); + std::copy(m_Data.get(), m_Data.get() + m_Size, newData.get()); + m_Data = std::move(newData); + m_Capacity = capacity; + } + + public: + explicit List(const size_t capacity = 3) : m_Capacity(capacity) { reserve(capacity); } + void own(T* data, const size_t size) { m_Data = data; m_Size = size; } + void copy(T* data, const size_t size) { + m_Data = std::make_unique(size); + std::copy(data, data + size, m_Data.get()); + m_Size = size; + } + + [[nodiscard]] size_t size() const { return m_Size; } + + T& operator[](size_t index) { return m_Data.get()[index]; } + const T& operator[](size_t index) const { return m_Data.get()[index]; } + + void need(const size_t additional_size) + { + if (m_Size + additional_size > m_Capacity) + reserve(m_Capacity + additional_size); + } + + [[nodiscard]] bool empty() const { return m_Size == 0; } + void clear() { m_Size = 0; } + + void push_back(const T& value) + { + if (m_Size >= m_Capacity) + reserve(m_Capacity * growth_scalar + growth_summand); + m_Data.get()[m_Size++] = value; + } + + template + void emplace_back(Args&&... args) + { + if (m_Size >= m_Capacity) + reserve(m_Capacity * growth_scalar + growth_summand); + m_Data.get()[m_Size++] = T(std::forward(args)...); + } + + void expand(const List& other) + { + need(other.size()); + std::copy(other.m_Data.get(), other.m_Data.get() + other.size(), m_Data.get() + m_Size); + m_Size += other.size(); + } + + void iterate(const std::function& func) + { + for (size_t i = 0; i < m_Size; i++) + func(m_Data.get()[i]); + } + + void iterate(const std::function& func) const + { + for (size_t i = 0; i < m_Size; i++) + func(m_Data.get()[i]); + } + }; + } + + using json = nlohmann::json; + + template + union Vector2 { + struct { T x, y; }; + struct { T a, b; }; + }; + + TYPEDEF_VECTORS(Vector2, 2) + + template + union Vector3 { + struct { T x, y, z; }; + struct { T r, g, b; }; + }; + TYPEDEF_VECTORS(Vector3, 3) + + template + union Vector4 { + struct { T x, y, z, w; }; + struct { T r, g, b, a; }; + struct { T left, top, right, bottom; }; + }; + TYPEDEF_VECTORS(Vector4, 4) + + + + class RenderCall + { + std::shared_ptr drawable; + sf::RenderStates states; + + public: + explicit RenderCall(const sf::RenderStates& states, sf::Drawable* ptr) : drawable(ptr), states(states) + { + if (!drawable) { throw std::runtime_error("RenderCall::RenderCall(): Drawable is null"); } + } + + void draw(sf::RenderTarget& target, const sf::RenderStates& states) const { target.draw(*drawable, states); } + }; + + enum class LayoutSizes : uint8_t + { + None = 0, + Min, + Max, + Fixed, + }; + + enum class LayoutAlignment : uint8_t + { + None = 0, + Left, + Right, + Top, + Bottom, + Center, + }; + + enum class LayoutDirection : uint8_t + { + None = 0, + LeftToRight, + RightToLeft, + TopToBottom, + BottomToTop, + }; + + enum class LayoutPosition : uint8_t + { + None = 0, + Absolute, + Left, + Right, + Top, + Bottom, + Center, + }; + + struct Layout + { + struct { + LayoutSizes rule = LayoutSizes::None; + vec2f minSize = {0.f, 0.f}; + vec2f maxSize = {0.f, 0.f}; + } size; + + struct { + LayoutAlignment rule = LayoutAlignment::None; + vec4f padding = {0.f, 0.f, 0.f, 0.f}; + vec4f margin = {0.f, 0.f, 0.f, 0.f}; + } alignment; + + struct { + LayoutDirection rule = LayoutDirection::None; + vec2f spacing = {0.f, 0.f}; + } direction; + + struct { + LayoutPosition rule = LayoutPosition::None; + vec2f position = {0.f, 0.f}; + } position; + }; + + class View + { + protected: + utils::List m_RenderCalls; + Layout m_Layout; + + public: + virtual ~View() = default; + + [[nodiscard]] const utils::List& renderCalls() const { return m_RenderCalls; } + std::unique_ptr content; + + virtual bool update() { if (content) return content->update(); return false; } + + void draw() + { + if (!m_RenderCalls.empty()) { m_RenderCalls.clear(); } + content = std::unique_ptr(body()); + if (!content) { return; } + content->draw(); + auto& contentRenderCalls = content->renderCalls(); + m_RenderCalls.expand(contentRenderCalls); + } + + virtual View* body() = 0; + }; + + struct Rectangle : View + { + sf::Color m_Color; + sf::Color m_BorderColor; + float m_BorderThickness = 0; + + Rectangle* setBorderColor(const sf::Color& color) { m_BorderColor = color; return this; } + Rectangle* setBorderThickness(float thickness) { m_BorderThickness = thickness; return this; } + Rectangle* setColor(const sf::Color& color) { m_Color = color; return this; } + Rectangle* setSize(const vec2f& size) { m_Layout.size.minSize = size; return this; } + + View* body() override { + auto m_Shape = new sf::RectangleShape(sf::Vector2f{m_Layout.size.minSize.x, m_Layout.size.minSize.y}); + m_Shape->setFillColor(m_Color); + if (m_BorderThickness > 0) + { + m_Shape->setOutlineThickness(m_BorderThickness); + m_Shape->setOutlineColor(m_BorderColor); + } + m_RenderCalls.emplace_back(sf::RenderStates(), m_Shape); + return nullptr; + } + + explicit Rectangle(const vec2f& size) { + m_Layout.size.minSize = size; + } + }; + + class Renderer + { + public: + explicit Renderer(View* _view) : view(_view) { view->draw(); } + + std::unique_ptr view; + + void update() const + { + if (view->update()) + view->draw(); + } + + void render(sf::RenderTarget& target, const sf::RenderStates& states) const + { + view->renderCalls().iterate([&target, &states](const RenderCall& renderCall) { renderCall.draw(target, states); }); + } + }; +} diff --git a/cmake/CIGUIConfig.cmake.in b/cmake/CIGUIConfig.cmake.in deleted file mode 100644 index 9dd02ff..0000000 --- a/cmake/CIGUIConfig.cmake.in +++ /dev/null @@ -1,16 +0,0 @@ -@PACKAGE_INIT@ - -# Import targets created by CIGUITargets.cmake -include("${CMAKE_CURRENT_LIST_DIR}/CIGUITargets.cmake") - -# Ensure SFML is available -include(CMakeFindDependencyMacro) -find_dependency(SFML 3 COMPONENTS graphics window system) - -# Define convenient imported target if it doesn't exist -if(NOT TARGET cigui::cigui) - add_library(cigui::cigui ALIAS cigui) -endif() - -# Check all required components are found -check_required_components(cigui) \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt deleted file mode 100644 index ca6c628..0000000 --- a/examples/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Examples CMakeLists.txt -cmake_minimum_required(VERSION 3.14) - -# Function to easily add examples -function(add_cigui_example NAME) - find_files(EXAMPLE_${NAME}_SOURCES "${NAME}/src" cpp c cxx hpp h hxx inl) - add_executable(EXAMPLE_${NAME} ${EXAMPLE_${NAME}_SOURCES}) - target_link_libraries(EXAMPLE_${NAME} PRIVATE cigui) - - set_target_properties(EXAMPLE_${NAME} PROPERTIES OUTPUT_NAME "${NAME}") - - # Copy SFML DLLs to output directory on Windows when building shared - if(WIN32 AND CIGUI_BUILD_SHARED) - add_custom_command(TARGET EXAMPLE_${NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - $ - $ - $ - ) - endif() -endfunction() - -# Basic example -add_subdirectory(Full) \ No newline at end of file diff --git a/examples/Full/CMakeLists.txt b/examples/Full/CMakeLists.txt deleted file mode 100644 index 531f664..0000000 --- a/examples/Full/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -add_cigui_example(General) -add_cigui_example(TicTacToe) \ No newline at end of file diff --git a/examples/Full/TicTacToe/src/main.cpp b/examples/Full/TicTacToe/src/main.cpp deleted file mode 100644 index 99707b4..0000000 --- a/examples/Full/TicTacToe/src/main.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include - -int main() { - std::cout << "Hello World" << std::endl; - return 0; -} diff --git a/include/argparse.hpp b/include/argparse.hpp new file mode 100644 index 0000000..06d30fd --- /dev/null +++ b/include/argparse.hpp @@ -0,0 +1,2589 @@ +/* + __ _ _ __ __ _ _ __ __ _ _ __ ___ ___ + / _` | '__/ _` | '_ \ / _` | '__/ __|/ _ \ Argument Parser for Modern C++ +| (_| | | | (_| | |_) | (_| | | \__ \ __/ http://github.com/p-ranav/argparse + \__,_|_| \__, | .__/ \__,_|_| |___/\___| + |___/|_| + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019-2022 Pranav Srinivas Kumar +and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#pragma once + +#include + +#ifndef ARGPARSE_MODULE_USE_STD_MODULE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifndef ARGPARSE_CUSTOM_STRTOF +#define ARGPARSE_CUSTOM_STRTOF strtof +#endif + +#ifndef ARGPARSE_CUSTOM_STRTOD +#define ARGPARSE_CUSTOM_STRTOD strtod +#endif + +#ifndef ARGPARSE_CUSTOM_STRTOLD +#define ARGPARSE_CUSTOM_STRTOLD strtold +#endif + +namespace argparse { + +namespace details { // namespace for helper methods + +template +struct HasContainerTraits : std::false_type {}; + +template <> struct HasContainerTraits : std::false_type {}; + +template <> struct HasContainerTraits : std::false_type {}; + +template +struct HasContainerTraits< + T, std::void_t().begin()), + decltype(std::declval().end()), + decltype(std::declval().size())>> : std::true_type {}; + +template +inline constexpr bool IsContainer = HasContainerTraits::value; + +template +struct HasStreamableTraits : std::false_type {}; + +template +struct HasStreamableTraits< + T, + std::void_t() << std::declval())>> + : std::true_type {}; + +template +inline constexpr bool IsStreamable = HasStreamableTraits::value; + +constexpr std::size_t repr_max_container_size = 5; + +template std::string repr(T const &val) { + if constexpr (std::is_same_v) { + return val ? "true" : "false"; + } else if constexpr (std::is_convertible_v) { + return '"' + std::string{std::string_view{val}} + '"'; + } else if constexpr (IsContainer) { + std::stringstream out; + out << "{"; + const auto size = val.size(); + if (size > 1) { + out << repr(*val.begin()); + std::for_each( + std::next(val.begin()), + std::next( + val.begin(), + static_cast( + std::min(size, repr_max_container_size) - 1)), + [&out](const auto &v) { out << " " << repr(v); }); + if (size <= repr_max_container_size) { + out << " "; + } else { + out << "..."; + } + } + if (size > 0) { + out << repr(*std::prev(val.end())); + } + out << "}"; + return out.str(); + } else if constexpr (IsStreamable) { + std::stringstream out; + out << val; + return out.str(); + } else { + return ""; + } +} + +namespace { + +template constexpr bool standard_signed_integer = false; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; +template <> constexpr bool standard_signed_integer = true; + +template constexpr bool standard_unsigned_integer = false; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> constexpr bool standard_unsigned_integer = true; +template <> +constexpr bool standard_unsigned_integer = true; + +} // namespace + +constexpr int radix_2 = 2; +constexpr int radix_8 = 8; +constexpr int radix_10 = 10; +constexpr int radix_16 = 16; + +template +constexpr bool standard_integer = + standard_signed_integer || standard_unsigned_integer; + +template +constexpr decltype(auto) +apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x, + std::index_sequence /*unused*/) { + return std::invoke(std::forward(f), std::get(std::forward(t))..., + std::forward(x)); +} + +template +constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) { + return details::apply_plus_one_impl( + std::forward(f), std::forward(t), std::forward(x), + std::make_index_sequence< + std::tuple_size_v>>{}); +} + +constexpr auto pointer_range(std::string_view s) noexcept { + return std::tuple(s.data(), s.data() + s.size()); +} + +template +constexpr bool starts_with(std::basic_string_view prefix, + std::basic_string_view s) noexcept { + return s.substr(0, prefix.size()) == prefix; +} + +enum class chars_format { + scientific = 0xf1, + fixed = 0xf2, + hex = 0xf4, + binary = 0xf8, + general = fixed | scientific +}; + +struct ConsumeBinaryPrefixResult { + bool is_binary; + std::string_view rest; +}; + +constexpr auto consume_binary_prefix(std::string_view s) + -> ConsumeBinaryPrefixResult { + if (starts_with(std::string_view{"0b"}, s) || + starts_with(std::string_view{"0B"}, s)) { + s.remove_prefix(2); + return {true, s}; + } + return {false, s}; +} + +struct ConsumeHexPrefixResult { + bool is_hexadecimal; + std::string_view rest; +}; + +using namespace std::literals; + +constexpr auto consume_hex_prefix(std::string_view s) + -> ConsumeHexPrefixResult { + if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { + s.remove_prefix(2); + return {true, s}; + } + return {false, s}; +} + +template +inline auto do_from_chars(std::string_view s) -> T { + T x{0}; + auto [first, last] = pointer_range(s); + auto [ptr, ec] = std::from_chars(first, last, x, Param); + if (ec == std::errc()) { + if (ptr == last) { + return x; + } + throw std::invalid_argument{"pattern '" + std::string(s) + + "' does not match to the end"}; + } + if (ec == std::errc::invalid_argument) { + throw std::invalid_argument{"pattern '" + std::string(s) + "' not found"}; + } + if (ec == std::errc::result_out_of_range) { + throw std::range_error{"'" + std::string(s) + "' not representable"}; + } + return x; // unreachable +} + +template struct parse_number { + auto operator()(std::string_view s) -> T { + return do_from_chars(s); + } +}; + +template struct parse_number { + auto operator()(std::string_view s) -> T { + if (auto [ok, rest] = consume_binary_prefix(s); ok) { + return do_from_chars(rest); + } + throw std::invalid_argument{"pattern not found"}; + } +}; + +template struct parse_number { + auto operator()(std::string_view s) -> T { + if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { + if (auto [ok, rest] = consume_hex_prefix(s); ok) { + try { + return do_from_chars(rest); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + std::string(s) + + "' as hexadecimal: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + std::string(s) + + "' as hexadecimal: " + err.what()); + } + } + } else { + // Allow passing hex numbers without prefix + // Shape 'x' already has to be specified + try { + return do_from_chars(s); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + std::string(s) + + "' as hexadecimal: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + std::string(s) + + "' as hexadecimal: " + err.what()); + } + } + + throw std::invalid_argument{"pattern '" + std::string(s) + + "' not identified as hexadecimal"}; + } +}; + +template struct parse_number { + auto operator()(std::string_view s) -> T { + auto [ok, rest] = consume_hex_prefix(s); + if (ok) { + try { + return do_from_chars(rest); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + std::string(s) + + "' as hexadecimal: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + std::string(s) + + "' as hexadecimal: " + err.what()); + } + } + + auto [ok_binary, rest_binary] = consume_binary_prefix(s); + if (ok_binary) { + try { + return do_from_chars(rest_binary); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + std::string(s) + + "' as binary: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + std::string(s) + + "' as binary: " + err.what()); + } + } + + if (starts_with("0"sv, s)) { + try { + return do_from_chars(rest); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + std::string(s) + + "' as octal: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + std::string(s) + + "' as octal: " + err.what()); + } + } + + try { + return do_from_chars(rest); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + std::string(s) + + "' as decimal integer: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + std::string(s) + + "' as decimal integer: " + err.what()); + } + } +}; + +namespace { + +template inline const auto generic_strtod = nullptr; +template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOF; +template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOD; +template <> +inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOLD; + +} // namespace + +template inline auto do_strtod(std::string const &s) -> T { + if (isspace(static_cast(s[0])) || s[0] == '+') { + throw std::invalid_argument{"pattern '" + s + "' not found"}; + } + + auto [first, last] = pointer_range(s); + char *ptr; + + errno = 0; + auto x = generic_strtod(first, &ptr); + if (errno == 0) { + if (ptr == last) { + return x; + } + throw std::invalid_argument{"pattern '" + s + + "' does not match to the end"}; + } + if (errno == ERANGE) { + throw std::range_error{"'" + s + "' not representable"}; + } + return x; // unreachable +} + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { + throw std::invalid_argument{ + "chars_format::general does not parse hexfloat"}; + } + if (auto r = consume_binary_prefix(s); r.is_binary) { + throw std::invalid_argument{ + "chars_format::general does not parse binfloat"}; + } + + try { + return do_strtod(s); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + s + + "' as number: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + s + + "' as number: " + err.what()); + } + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); !r.is_hexadecimal) { + throw std::invalid_argument{"chars_format::hex parses hexfloat"}; + } + if (auto r = consume_binary_prefix(s); r.is_binary) { + throw std::invalid_argument{"chars_format::hex does not parse binfloat"}; + } + + try { + return do_strtod(s); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + s + + "' as hexadecimal: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + s + + "' as hexadecimal: " + err.what()); + } + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { + throw std::invalid_argument{ + "chars_format::binary does not parse hexfloat"}; + } + if (auto r = consume_binary_prefix(s); !r.is_binary) { + throw std::invalid_argument{"chars_format::binary parses binfloat"}; + } + + return do_strtod(s); + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { + throw std::invalid_argument{ + "chars_format::scientific does not parse hexfloat"}; + } + if (auto r = consume_binary_prefix(s); r.is_binary) { + throw std::invalid_argument{ + "chars_format::scientific does not parse binfloat"}; + } + if (s.find_first_of("eE") == std::string::npos) { + throw std::invalid_argument{ + "chars_format::scientific requires exponent part"}; + } + + try { + return do_strtod(s); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + s + + "' as scientific notation: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + s + + "' as scientific notation: " + err.what()); + } + } +}; + +template struct parse_number { + auto operator()(std::string const &s) -> T { + if (auto r = consume_hex_prefix(s); r.is_hexadecimal) { + throw std::invalid_argument{ + "chars_format::fixed does not parse hexfloat"}; + } + if (auto r = consume_binary_prefix(s); r.is_binary) { + throw std::invalid_argument{ + "chars_format::fixed does not parse binfloat"}; + } + if (s.find_first_of("eE") != std::string::npos) { + throw std::invalid_argument{ + "chars_format::fixed does not parse exponent part"}; + } + + try { + return do_strtod(s); + } catch (const std::invalid_argument &err) { + throw std::invalid_argument("Failed to parse '" + s + + "' as fixed notation: " + err.what()); + } catch (const std::range_error &err) { + throw std::range_error("Failed to parse '" + s + + "' as fixed notation: " + err.what()); + } + } +}; + +template +std::string join(StrIt first, StrIt last, const std::string &separator) { + if (first == last) { + return ""; + } + std::stringstream value; + value << *first; + ++first; + while (first != last) { + value << separator << *first; + ++first; + } + return value.str(); +} + +template struct can_invoke_to_string { + template + static auto test(int) + -> decltype(std::to_string(std::declval()), std::true_type{}); + + template static auto test(...) -> std::false_type; + + static constexpr bool value = decltype(test(0))::value; +}; + +template struct IsChoiceTypeSupported { + using CleanType = typename std::decay::type; + static const bool value = std::is_integral::value || + std::is_same::value || + std::is_same::value || + std::is_same::value; +}; + +template +std::size_t get_levenshtein_distance(const StringType &s1, + const StringType &s2) { + std::vector> dp( + s1.size() + 1, std::vector(s2.size() + 1, 0)); + + for (std::size_t i = 0; i <= s1.size(); ++i) { + for (std::size_t j = 0; j <= s2.size(); ++j) { + if (i == 0) { + dp[i][j] = j; + } else if (j == 0) { + dp[i][j] = i; + } else if (s1[i - 1] == s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}); + } + } + } + + return dp[s1.size()][s2.size()]; +} + +template +std::string get_most_similar_string(const std::map &map, + const std::string &input) { + std::string most_similar{}; + std::size_t min_distance = (std::numeric_limits::max)(); + + for (const auto &entry : map) { + std::size_t distance = get_levenshtein_distance(entry.first, input); + if (distance < min_distance) { + min_distance = distance; + most_similar = entry.first; + } + } + + return most_similar; +} + +} // namespace details + +enum class nargs_pattern { optional, any, at_least_one }; + +enum class default_arguments : unsigned int { + none = 0, + help = 1, + version = 2, + all = help | version, +}; + +inline default_arguments operator&(const default_arguments &a, + const default_arguments &b) { + return static_cast( + static_cast::type>(a) & + static_cast::type>(b)); +} + +class ArgumentParser; + +class Argument { + friend class ArgumentParser; + friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) + -> std::ostream &; + + template + explicit Argument(std::string_view prefix_chars, + std::array &&a, + std::index_sequence /*unused*/) + : m_accepts_optional_like_value(false), + m_is_optional((is_optional(a[I], prefix_chars) || ...)), + m_is_required(false), m_is_repeatable(false), m_is_used(false), + m_is_hidden(false), m_prefix_chars(prefix_chars) { + ((void)m_names.emplace_back(a[I]), ...); + std::sort( + m_names.begin(), m_names.end(), [](const auto &lhs, const auto &rhs) { + return lhs.size() == rhs.size() ? lhs < rhs : lhs.size() < rhs.size(); + }); + } + +public: + template + explicit Argument(std::string_view prefix_chars, + std::array &&a) + : Argument(prefix_chars, std::move(a), std::make_index_sequence{}) {} + + Argument &help(std::string help_text) { + m_help = std::move(help_text); + return *this; + } + + Argument &metavar(std::string metavar) { + m_metavar = std::move(metavar); + return *this; + } + + template Argument &default_value(T &&value) { + m_num_args_range = NArgsRange{0, m_num_args_range.get_max()}; + m_default_value_repr = details::repr(value); + + if constexpr (std::is_convertible_v) { + m_default_value_str = std::string{std::string_view{value}}; + } else if constexpr (details::can_invoke_to_string::value) { + m_default_value_str = std::to_string(value); + } + + m_default_value = std::forward(value); + return *this; + } + + Argument &default_value(const char *value) { + return default_value(std::string(value)); + } + + Argument &required() { + m_is_required = true; + return *this; + } + + Argument &implicit_value(std::any value) { + m_implicit_value = std::move(value); + m_num_args_range = NArgsRange{0, 0}; + return *this; + } + + // This is shorthand for: + // program.add_argument("foo") + // .default_value(false) + // .implicit_value(true) + Argument &flag() { + default_value(false); + implicit_value(true); + return *this; + } + + template + auto action(F &&callable, Args &&... bound_args) + -> std::enable_if_t, + Argument &> { + using action_type = std::conditional_t< + std::is_void_v>, + void_action, valued_action>; + if constexpr (sizeof...(Args) == 0) { + m_actions.emplace_back(std::forward(callable)); + } else { + m_actions.emplace_back( + [f = std::forward(callable), + tup = std::make_tuple(std::forward(bound_args)...)]( + std::string const &opt) mutable { + return details::apply_plus_one(f, tup, opt); + }); + } + return *this; + } + + auto &store_into(bool &var) { + if ((!m_default_value.has_value()) && (!m_implicit_value.has_value())) { + flag(); + } + if (m_default_value.has_value()) { + var = std::any_cast(m_default_value); + } + action([&var](const auto & /*unused*/) { + var = true; + return var; + }); + return *this; + } + + template ::value>::type * = nullptr> + auto &store_into(T &var) { + if (m_default_value.has_value()) { + var = std::any_cast(m_default_value); + } + action([&var](const auto &s) { + var = details::parse_number()(s); + return var; + }); + return *this; + } + + template ::value>::type * = nullptr> + auto &store_into(T &var) { + if (m_default_value.has_value()) { + var = std::any_cast(m_default_value); + } + action([&var](const auto &s) { + var = details::parse_number()(s); + return var; + }); + return *this; + } + + auto &store_into(std::string &var) { + if (m_default_value.has_value()) { + var = std::any_cast(m_default_value); + } + action([&var](const std::string &s) { + var = s; + return var; + }); + return *this; + } + + auto &store_into(std::filesystem::path &var) { + if (m_default_value.has_value()) { + var = std::any_cast(m_default_value); + } + action([&var](const std::string &s) { var = s; }); + return *this; + } + + auto &store_into(std::vector &var) { + if (m_default_value.has_value()) { + var = std::any_cast>(m_default_value); + } + action([this, &var](const std::string &s) { + if (!m_is_used) { + var.clear(); + } + m_is_used = true; + var.push_back(s); + return var; + }); + return *this; + } + + auto &store_into(std::vector &var) { + if (m_default_value.has_value()) { + var = std::any_cast>(m_default_value); + } + action([this, &var](const std::string &s) { + if (!m_is_used) { + var.clear(); + } + m_is_used = true; + var.push_back(details::parse_number()(s)); + return var; + }); + return *this; + } + + auto &store_into(std::set &var) { + if (m_default_value.has_value()) { + var = std::any_cast>(m_default_value); + } + action([this, &var](const std::string &s) { + if (!m_is_used) { + var.clear(); + } + m_is_used = true; + var.insert(s); + return var; + }); + return *this; + } + + auto &store_into(std::set &var) { + if (m_default_value.has_value()) { + var = std::any_cast>(m_default_value); + } + action([this, &var](const std::string &s) { + if (!m_is_used) { + var.clear(); + } + m_is_used = true; + var.insert(details::parse_number()(s)); + return var; + }); + return *this; + } + + auto &append() { + m_is_repeatable = true; + return *this; + } + + // Cause the argument to be invisible in usage and help + auto &hidden() { + m_is_hidden = true; + return *this; + } + + template + auto scan() -> std::enable_if_t, Argument &> { + static_assert(!(std::is_const_v || std::is_volatile_v), + "T should not be cv-qualified"); + auto is_one_of = [](char c, auto... x) constexpr { + return ((c == x) || ...); + }; + + if constexpr (is_one_of(Shape, 'd') && details::standard_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'i') && + details::standard_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'u') && + details::standard_unsigned_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'b') && + details::standard_unsigned_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'o') && + details::standard_unsigned_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'x', 'X') && + details::standard_unsigned_integer) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'a', 'A') && + std::is_floating_point_v) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'e', 'E') && + std::is_floating_point_v) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'f', 'F') && + std::is_floating_point_v) { + action(details::parse_number()); + } else if constexpr (is_one_of(Shape, 'g', 'G') && + std::is_floating_point_v) { + action(details::parse_number()); + } else { + static_assert(alignof(T) == 0, "No scan specification for T"); + } + + return *this; + } + + Argument &nargs(std::size_t num_args) { + m_num_args_range = NArgsRange{num_args, num_args}; + return *this; + } + + Argument &nargs(std::size_t num_args_min, std::size_t num_args_max) { + m_num_args_range = NArgsRange{num_args_min, num_args_max}; + return *this; + } + + Argument &nargs(nargs_pattern pattern) { + switch (pattern) { + case nargs_pattern::optional: + m_num_args_range = NArgsRange{0, 1}; + break; + case nargs_pattern::any: + m_num_args_range = + NArgsRange{0, (std::numeric_limits::max)()}; + break; + case nargs_pattern::at_least_one: + m_num_args_range = + NArgsRange{1, (std::numeric_limits::max)()}; + break; + } + return *this; + } + + Argument &remaining() { + m_accepts_optional_like_value = true; + return nargs(nargs_pattern::any); + } + + template void add_choice(T &&choice) { + static_assert(details::IsChoiceTypeSupported::value, + "Only string or integer type supported for choice"); + static_assert(std::is_convertible_v || + details::can_invoke_to_string::value, + "Choice is not convertible to string_type"); + if (!m_choices.has_value()) { + m_choices = std::vector{}; + } + + if constexpr (std::is_convertible_v) { + m_choices.value().push_back( + std::string{std::string_view{std::forward(choice)}}); + } else if constexpr (details::can_invoke_to_string::value) { + m_choices.value().push_back(std::to_string(std::forward(choice))); + } + } + + Argument &choices() { + if (!m_choices.has_value()) { + throw std::runtime_error("Zero choices provided"); + } + return *this; + } + + template + Argument &choices(T &&first, U &&... rest) { + add_choice(std::forward(first)); + choices(std::forward(rest)...); + return *this; + } + + void find_default_value_in_choices_or_throw() const { + + const auto &choices = m_choices.value(); + + if (m_default_value.has_value()) { + if (std::find(choices.begin(), choices.end(), m_default_value_str) == + choices.end()) { + // provided arg not in list of allowed choices + // report error + + std::string choices_as_csv = + std::accumulate(choices.begin(), choices.end(), std::string(), + [](const std::string &a, const std::string &b) { + return a + (a.empty() ? "" : ", ") + b; + }); + + throw std::runtime_error( + std::string{"Invalid default value "} + m_default_value_repr + + " - allowed options: {" + choices_as_csv + "}"); + } + } + } + + template + bool is_value_in_choices(Iterator option_it) const { + + const auto &choices = m_choices.value(); + + return (std::find(choices.begin(), choices.end(), *option_it) != + choices.end()); + } + + template + void throw_invalid_arguments_error(Iterator option_it) const { + const auto &choices = m_choices.value(); + const std::string choices_as_csv = std::accumulate( + choices.begin(), choices.end(), std::string(), + [](const std::string &option_a, const std::string &option_b) { + return option_a + (option_a.empty() ? "" : ", ") + option_b; + }); + + throw std::runtime_error(std::string{"Invalid argument "} + + details::repr(*option_it) + + " - allowed options: {" + choices_as_csv + "}"); + } + + /* The dry_run parameter can be set to true to avoid running the actions, + * and setting m_is_used. This may be used by a pre-processing step to do + * a first iteration over arguments. + */ + template + Iterator consume(Iterator start, Iterator end, + std::string_view used_name = {}, bool dry_run = false) { + if (!m_is_repeatable && m_is_used) { + throw std::runtime_error( + std::string("Duplicate argument ").append(used_name)); + } + m_used_name = used_name; + + std::size_t passed_options = 0; + + if (m_choices.has_value()) { + // Check each value in (start, end) and make sure + // it is in the list of allowed choices/options + const auto max_number_of_args = m_num_args_range.get_max(); + const auto min_number_of_args = m_num_args_range.get_min(); + for (auto it = start; it != end; ++it) { + if (is_value_in_choices(it)) { + passed_options += 1; + continue; + } + + if ((passed_options >= min_number_of_args) && + (passed_options <= max_number_of_args)) { + break; + } + + throw_invalid_arguments_error(it); + } + } + + const auto num_args_max = + (m_choices.has_value()) ? passed_options : m_num_args_range.get_max(); + const auto num_args_min = m_num_args_range.get_min(); + std::size_t dist = 0; + if (num_args_max == 0) { + if (!dry_run) { + m_values.emplace_back(m_implicit_value); + for(auto &action: m_actions) { + std::visit([&](const auto &f) { f({}); }, action); + } + if(m_actions.empty()){ + std::visit([&](const auto &f) { f({}); }, m_default_action); + } + m_is_used = true; + } + return start; + } + if ((dist = static_cast(std::distance(start, end))) >= + num_args_min) { + if (num_args_max < dist) { + end = std::next(start, static_cast( + num_args_max)); + } + if (!m_accepts_optional_like_value) { + end = std::find_if( + start, end, + std::bind(is_optional, std::placeholders::_1, m_prefix_chars)); + dist = static_cast(std::distance(start, end)); + if (dist < num_args_min) { + throw std::runtime_error("Too few arguments for '" + + std::string(m_used_name) + "'."); + } + } + struct ActionApply { + void operator()(valued_action &f) { + std::transform(first, last, std::back_inserter(self.m_values), f); + } + + void operator()(void_action &f) { + std::for_each(first, last, f); + if (!self.m_default_value.has_value()) { + if (!self.m_accepts_optional_like_value) { + self.m_values.resize( + static_cast(std::distance(first, last))); + } + } + } + + Iterator first, last; + Argument &self; + }; + if (!dry_run) { + for(auto &action: m_actions) { + std::visit(ActionApply{start, end, *this}, action); + } + if(m_actions.empty()){ + std::visit(ActionApply{start, end, *this}, m_default_action); + } + m_is_used = true; + } + return end; + } + if (m_default_value.has_value()) { + if (!dry_run) { + m_is_used = true; + } + return start; + } + throw std::runtime_error("Too few arguments for '" + + std::string(m_used_name) + "'."); + } + + /* + * @throws std::runtime_error if argument values are not valid + */ + void validate() const { + if (m_is_optional) { + // TODO: check if an implicit value was programmed for this argument + if (!m_is_used && !m_default_value.has_value() && m_is_required) { + throw_required_arg_not_used_error(); + } + if (m_is_used && m_is_required && m_values.empty()) { + throw_required_arg_no_value_provided_error(); + } + } else { + if (!m_num_args_range.contains(m_values.size()) && + !m_default_value.has_value()) { + throw_nargs_range_validation_error(); + } + } + + if (m_choices.has_value()) { + // Make sure the default value (if provided) + // is in the list of choices + find_default_value_in_choices_or_throw(); + } + } + + std::string get_names_csv(char separator = ',') const { + return std::accumulate( + m_names.begin(), m_names.end(), std::string{""}, + [&](const std::string &result, const std::string &name) { + return result.empty() ? name : result + separator + name; + }); + } + + std::string get_usage_full() const { + std::stringstream usage; + + usage << get_names_csv('/'); + const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR"; + if (m_num_args_range.get_max() > 0) { + usage << " " << metavar; + if (m_num_args_range.get_max() > 1) { + usage << "..."; + } + } + return usage.str(); + } + + std::string get_inline_usage() const { + std::stringstream usage; + // Find the longest variant to show in the usage string + std::string longest_name = m_names.front(); + for (const auto &s : m_names) { + if (s.size() > longest_name.size()) { + longest_name = s; + } + } + if (!m_is_required) { + usage << "["; + } + usage << longest_name; + const std::string metavar = !m_metavar.empty() ? m_metavar : "VAR"; + if (m_num_args_range.get_max() > 0) { + usage << " " << metavar; + if (m_num_args_range.get_max() > 1 && + m_metavar.find("> <") == std::string::npos) { + usage << "..."; + } + } + if (!m_is_required) { + usage << "]"; + } + if (m_is_repeatable) { + usage << "..."; + } + return usage.str(); + } + + std::size_t get_arguments_length() const { + + std::size_t names_size = std::accumulate( + std::begin(m_names), std::end(m_names), std::size_t(0), + [](const auto &sum, const auto &s) { return sum + s.size(); }); + + if (is_positional(m_names.front(), m_prefix_chars)) { + // A set metavar means this replaces the names + if (!m_metavar.empty()) { + // Indent and metavar + return 2 + m_metavar.size(); + } + + // Indent and space-separated + return 2 + names_size + (m_names.size() - 1); + } + // Is an option - include both names _and_ metavar + // size = text + (", " between names) + std::size_t size = names_size + 2 * (m_names.size() - 1); + if (!m_metavar.empty() && m_num_args_range == NArgsRange{1, 1}) { + size += m_metavar.size() + 1; + } + return size + 2; // indent + } + + friend std::ostream &operator<<(std::ostream &stream, + const Argument &argument) { + std::stringstream name_stream; + name_stream << " "; // indent + if (argument.is_positional(argument.m_names.front(), + argument.m_prefix_chars)) { + if (!argument.m_metavar.empty()) { + name_stream << argument.m_metavar; + } else { + name_stream << details::join(argument.m_names.begin(), + argument.m_names.end(), " "); + } + } else { + name_stream << details::join(argument.m_names.begin(), + argument.m_names.end(), ", "); + // If we have a metavar, and one narg - print the metavar + if (!argument.m_metavar.empty() && + argument.m_num_args_range == NArgsRange{1, 1}) { + name_stream << " " << argument.m_metavar; + } + else if (!argument.m_metavar.empty() && + argument.m_num_args_range.get_min() == argument.m_num_args_range.get_max() && + argument.m_metavar.find("> <") != std::string::npos) { + name_stream << " " << argument.m_metavar; + } + } + + // align multiline help message + auto stream_width = stream.width(); + auto name_padding = std::string(name_stream.str().size(), ' '); + auto pos = std::string::size_type{}; + auto prev = std::string::size_type{}; + auto first_line = true; + auto hspace = " "; // minimal space between name and help message + stream << name_stream.str(); + std::string_view help_view(argument.m_help); + while ((pos = argument.m_help.find('\n', prev)) != std::string::npos) { + auto line = help_view.substr(prev, pos - prev + 1); + if (first_line) { + stream << hspace << line; + first_line = false; + } else { + stream.width(stream_width); + stream << name_padding << hspace << line; + } + prev += pos - prev + 1; + } + if (first_line) { + stream << hspace << argument.m_help; + } else { + auto leftover = help_view.substr(prev, argument.m_help.size() - prev); + if (!leftover.empty()) { + stream.width(stream_width); + stream << name_padding << hspace << leftover; + } + } + + // print nargs spec + if (!argument.m_help.empty()) { + stream << " "; + } + stream << argument.m_num_args_range; + + bool add_space = false; + if (argument.m_default_value.has_value() && + argument.m_num_args_range != NArgsRange{0, 0}) { + stream << "[default: " << argument.m_default_value_repr << "]"; + add_space = true; + } else if (argument.m_is_required) { + stream << "[required]"; + add_space = true; + } + if (argument.m_is_repeatable) { + if (add_space) { + stream << " "; + } + stream << "[may be repeated]"; + } + stream << "\n"; + return stream; + } + + template bool operator!=(const T &rhs) const { + return !(*this == rhs); + } + + /* + * Compare to an argument value of known type + * @throws std::logic_error in case of incompatible types + */ + template bool operator==(const T &rhs) const { + if constexpr (!details::IsContainer) { + return get() == rhs; + } else { + using ValueType = typename T::value_type; + auto lhs = get(); + return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), + std::end(rhs), [](const auto &a, const auto &b) { + return std::any_cast(a) == b; + }); + } + } + + /* + * positional: + * _empty_ + * '-' + * '-' decimal-literal + * !'-' anything + */ + static bool is_positional(std::string_view name, + std::string_view prefix_chars) { + auto first = lookahead(name); + + if (first == eof) { + return true; + } + if (prefix_chars.find(static_cast(first)) != + std::string_view::npos) { + name.remove_prefix(1); + if (name.empty()) { + return true; + } + return is_decimal_literal(name); + } + return true; + } + +private: + class NArgsRange { + std::size_t m_min; + std::size_t m_max; + + public: + NArgsRange(std::size_t minimum, std::size_t maximum) + : m_min(minimum), m_max(maximum) { + if (minimum > maximum) { + throw std::logic_error("Range of number of arguments is invalid"); + } + } + + bool contains(std::size_t value) const { + return value >= m_min && value <= m_max; + } + + bool is_exact() const { return m_min == m_max; } + + bool is_right_bounded() const { + return m_max < (std::numeric_limits::max)(); + } + + std::size_t get_min() const { return m_min; } + + std::size_t get_max() const { return m_max; } + + // Print help message + friend auto operator<<(std::ostream &stream, const NArgsRange &range) + -> std::ostream & { + if (range.m_min == range.m_max) { + if (range.m_min != 0 && range.m_min != 1) { + stream << "[nargs: " << range.m_min << "] "; + } + } else { + if (range.m_max == (std::numeric_limits::max)()) { + stream << "[nargs: " << range.m_min << " or more] "; + } else { + stream << "[nargs=" << range.m_min << ".." << range.m_max << "] "; + } + } + return stream; + } + + bool operator==(const NArgsRange &rhs) const { + return rhs.m_min == m_min && rhs.m_max == m_max; + } + + bool operator!=(const NArgsRange &rhs) const { return !(*this == rhs); } + }; + + void throw_nargs_range_validation_error() const { + std::stringstream stream; + if (!m_used_name.empty()) { + stream << m_used_name << ": "; + } else { + stream << m_names.front() << ": "; + } + if (m_num_args_range.is_exact()) { + stream << m_num_args_range.get_min(); + } else if (m_num_args_range.is_right_bounded()) { + stream << m_num_args_range.get_min() << " to " + << m_num_args_range.get_max(); + } else { + stream << m_num_args_range.get_min() << " or more"; + } + stream << " argument(s) expected. " << m_values.size() << " provided."; + throw std::runtime_error(stream.str()); + } + + void throw_required_arg_not_used_error() const { + std::stringstream stream; + stream << m_names.front() << ": required."; + throw std::runtime_error(stream.str()); + } + + void throw_required_arg_no_value_provided_error() const { + std::stringstream stream; + stream << m_used_name << ": no value provided."; + throw std::runtime_error(stream.str()); + } + + static constexpr int eof = std::char_traits::eof(); + + static auto lookahead(std::string_view s) -> int { + if (s.empty()) { + return eof; + } + return static_cast(static_cast(s[0])); + } + + /* + * decimal-literal: + * '0' + * nonzero-digit digit-sequence_opt + * integer-part fractional-part + * fractional-part + * integer-part '.' exponent-part_opt + * integer-part exponent-part + * + * integer-part: + * digit-sequence + * + * fractional-part: + * '.' post-decimal-point + * + * post-decimal-point: + * digit-sequence exponent-part_opt + * + * exponent-part: + * 'e' post-e + * 'E' post-e + * + * post-e: + * sign_opt digit-sequence + * + * sign: one of + * '+' '-' + */ + static bool is_decimal_literal(std::string_view s) { + auto is_digit = [](auto c) constexpr { + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return true; + default: + return false; + } + }; + + // precondition: we have consumed or will consume at least one digit + auto consume_digits = [=](std::string_view sd) { + // NOLINTNEXTLINE(readability-qualified-auto) + auto it = std::find_if_not(std::begin(sd), std::end(sd), is_digit); + return sd.substr(static_cast(it - std::begin(sd))); + }; + + switch (lookahead(s)) { + case '0': { + s.remove_prefix(1); + if (s.empty()) { + return true; + } + goto integer_part; + } + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': { + s = consume_digits(s); + if (s.empty()) { + return true; + } + goto integer_part_consumed; + } + case '.': { + s.remove_prefix(1); + goto post_decimal_point; + } + default: + return false; + } + + integer_part: + s = consume_digits(s); + integer_part_consumed: + switch (lookahead(s)) { + case '.': { + s.remove_prefix(1); + if (is_digit(lookahead(s))) { + goto post_decimal_point; + } else { + goto exponent_part_opt; + } + } + case 'e': + case 'E': { + s.remove_prefix(1); + goto post_e; + } + default: + return false; + } + + post_decimal_point: + if (is_digit(lookahead(s))) { + s = consume_digits(s); + goto exponent_part_opt; + } + return false; + + exponent_part_opt: + switch (lookahead(s)) { + case eof: + return true; + case 'e': + case 'E': { + s.remove_prefix(1); + goto post_e; + } + default: + return false; + } + + post_e: + switch (lookahead(s)) { + case '-': + case '+': + s.remove_prefix(1); + } + if (is_digit(lookahead(s))) { + s = consume_digits(s); + return s.empty(); + } + return false; + } + + static bool is_optional(std::string_view name, + std::string_view prefix_chars) { + return !is_positional(name, prefix_chars); + } + + /* + * Get argument value given a type + * @throws std::logic_error in case of incompatible types + */ + template T get() const { + if (!m_values.empty()) { + if constexpr (details::IsContainer) { + return any_cast_container(m_values); + } else { + return std::any_cast(m_values.front()); + } + } + if (m_default_value.has_value()) { + return std::any_cast(m_default_value); + } + if constexpr (details::IsContainer) { + if (!m_accepts_optional_like_value) { + return any_cast_container(m_values); + } + } + + throw std::logic_error("No value provided for '" + m_names.back() + "'."); + } + + /* + * Get argument value given a type. + * @pre The object has no default value. + * @returns The stored value if any, std::nullopt otherwise. + */ + template auto present() const -> std::optional { + if (m_default_value.has_value()) { + throw std::logic_error("Argument with default value always presents"); + } + if (m_values.empty()) { + return std::nullopt; + } + if constexpr (details::IsContainer) { + return any_cast_container(m_values); + } + return std::any_cast(m_values.front()); + } + + template + static auto any_cast_container(const std::vector &operand) -> T { + using ValueType = typename T::value_type; + + T result; + std::transform( + std::begin(operand), std::end(operand), std::back_inserter(result), + [](const auto &value) { return std::any_cast(value); }); + return result; + } + + void set_usage_newline_counter(int i) { m_usage_newline_counter = i; } + + void set_group_idx(std::size_t i) { m_group_idx = i; } + + std::vector m_names; + std::string_view m_used_name; + std::string m_help; + std::string m_metavar; + std::any m_default_value; + std::string m_default_value_repr; + std::optional + m_default_value_str; // used for checking default_value against choices + std::any m_implicit_value; + std::optional> m_choices{std::nullopt}; + using valued_action = std::function; + using void_action = std::function; + std::vector> m_actions; + std::variant m_default_action{ + std::in_place_type, + [](const std::string &value) { return value; }}; + std::vector m_values; + NArgsRange m_num_args_range{1, 1}; + // Bit field of bool values. Set default value in ctor. + bool m_accepts_optional_like_value : 1; + bool m_is_optional : 1; + bool m_is_required : 1; + bool m_is_repeatable : 1; + bool m_is_used : 1; + bool m_is_hidden : 1; // if set, does not appear in usage or help + std::string_view m_prefix_chars; // ArgumentParser has the prefix_chars + int m_usage_newline_counter = 0; + std::size_t m_group_idx = 0; +}; + +class ArgumentParser { +public: + explicit ArgumentParser(std::string program_name = {}, + std::string version = "1.0", + default_arguments add_args = default_arguments::all, + bool exit_on_default_arguments = true, + std::ostream &os = std::cout) + : m_program_name(std::move(program_name)), m_version(std::move(version)), + m_exit_on_default_arguments(exit_on_default_arguments), + m_parser_path(m_program_name) { + if ((add_args & default_arguments::help) == default_arguments::help) { + add_argument("-h", "--help") + .action([&](const auto & /*unused*/) { + os << help().str(); + if (m_exit_on_default_arguments) { + std::exit(0); + } + }) + .default_value(false) + .help("shows help message and exits") + .implicit_value(true) + .nargs(0); + } + if ((add_args & default_arguments::version) == default_arguments::version) { + add_argument("-v", "--version") + .action([&](const auto & /*unused*/) { + os << m_version << std::endl; + if (m_exit_on_default_arguments) { + std::exit(0); + } + }) + .default_value(false) + .help("prints version information and exits") + .implicit_value(true) + .nargs(0); + } + } + + ~ArgumentParser() = default; + + // ArgumentParser is meant to be used in a single function. + // Setup everything and parse arguments in one place. + // + // ArgumentParser internally uses std::string_views, + // references, iterators, etc. + // Many of these elements become invalidated after a copy or move. + ArgumentParser(const ArgumentParser &other) = delete; + ArgumentParser &operator=(const ArgumentParser &other) = delete; + ArgumentParser(ArgumentParser &&) noexcept = delete; + ArgumentParser &operator=(ArgumentParser &&) = delete; + + explicit operator bool() const { + auto arg_used = std::any_of(m_argument_map.cbegin(), m_argument_map.cend(), + [](auto &it) { return it.second->m_is_used; }); + auto subparser_used = + std::any_of(m_subparser_used.cbegin(), m_subparser_used.cend(), + [](auto &it) { return it.second; }); + + return m_is_parsed && (arg_used || subparser_used); + } + + // Parameter packing + // Call add_argument with variadic number of string arguments + template Argument &add_argument(Targs... f_args) { + using array_of_sv = std::array; + auto argument = + m_optional_arguments.emplace(std::cend(m_optional_arguments), + m_prefix_chars, array_of_sv{f_args...}); + + if (!argument->m_is_optional) { + m_positional_arguments.splice(std::cend(m_positional_arguments), + m_optional_arguments, argument); + } + argument->set_usage_newline_counter(m_usage_newline_counter); + argument->set_group_idx(m_group_names.size()); + + index_argument(argument); + return *argument; + } + + class MutuallyExclusiveGroup { + friend class ArgumentParser; + + public: + MutuallyExclusiveGroup() = delete; + + explicit MutuallyExclusiveGroup(ArgumentParser &parent, + bool required = false) + : m_parent(parent), m_required(required), m_elements({}) {} + + MutuallyExclusiveGroup(const MutuallyExclusiveGroup &other) = delete; + MutuallyExclusiveGroup & + operator=(const MutuallyExclusiveGroup &other) = delete; + + MutuallyExclusiveGroup(MutuallyExclusiveGroup &&other) noexcept + : m_parent(other.m_parent), m_required(other.m_required), + m_elements(std::move(other.m_elements)) { + other.m_elements.clear(); + } + + template Argument &add_argument(Targs... f_args) { + auto &argument = m_parent.add_argument(std::forward(f_args)...); + m_elements.push_back(&argument); + argument.set_usage_newline_counter(m_parent.m_usage_newline_counter); + argument.set_group_idx(m_parent.m_group_names.size()); + return argument; + } + + private: + ArgumentParser &m_parent; + bool m_required{false}; + std::vector m_elements{}; + }; + + MutuallyExclusiveGroup &add_mutually_exclusive_group(bool required = false) { + m_mutually_exclusive_groups.emplace_back(*this, required); + return m_mutually_exclusive_groups.back(); + } + + // Parameter packed add_parents method + // Accepts a variadic number of ArgumentParser objects + template + ArgumentParser &add_parents(const Targs &... f_args) { + for (const ArgumentParser &parent_parser : {std::ref(f_args)...}) { + for (const auto &argument : parent_parser.m_positional_arguments) { + auto it = m_positional_arguments.insert( + std::cend(m_positional_arguments), argument); + index_argument(it); + } + for (const auto &argument : parent_parser.m_optional_arguments) { + auto it = m_optional_arguments.insert(std::cend(m_optional_arguments), + argument); + index_argument(it); + } + } + return *this; + } + + // Ask for the next optional arguments to be displayed on a separate + // line in usage() output. Only effective if set_usage_max_line_width() is + // also used. + ArgumentParser &add_usage_newline() { + ++m_usage_newline_counter; + return *this; + } + + // Ask for the next optional arguments to be displayed in a separate section + // in usage() and help (<< *this) output. + // For usage(), this is only effective if set_usage_max_line_width() is + // also used. + ArgumentParser &add_group(std::string group_name) { + m_group_names.emplace_back(std::move(group_name)); + return *this; + } + + ArgumentParser &add_description(std::string description) { + m_description = std::move(description); + return *this; + } + + ArgumentParser &add_epilog(std::string epilog) { + m_epilog = std::move(epilog); + return *this; + } + + // Add a un-documented/hidden alias for an argument. + // Ideally we'd want this to be a method of Argument, but Argument + // does not own its owing ArgumentParser. + ArgumentParser &add_hidden_alias_for(Argument &arg, std::string_view alias) { + for (auto it = m_optional_arguments.begin(); + it != m_optional_arguments.end(); ++it) { + if (&(*it) == &arg) { + m_argument_map.insert_or_assign(std::string(alias), it); + return *this; + } + } + throw std::logic_error( + "Argument is not an optional argument of this parser"); + } + + /* Getter for arguments and subparsers. + * @throws std::logic_error in case of an invalid argument or subparser name + */ + template T &at(std::string_view name) { + if constexpr (std::is_same_v) { + return (*this)[name]; + } else { + std::string str_name(name); + auto subparser_it = m_subparser_map.find(str_name); + if (subparser_it != m_subparser_map.end()) { + return subparser_it->second->get(); + } + throw std::logic_error("No such subparser: " + str_name); + } + } + + ArgumentParser &set_prefix_chars(std::string prefix_chars) { + m_prefix_chars = std::move(prefix_chars); + return *this; + } + + ArgumentParser &set_assign_chars(std::string assign_chars) { + m_assign_chars = std::move(assign_chars); + return *this; + } + + /* Call parse_args_internal - which does all the work + * Then, validate the parsed arguments + * This variant is used mainly for testing + * @throws std::runtime_error in case of any invalid argument + */ + void parse_args(const std::vector &arguments) { + parse_args_internal(arguments); + // Check if all arguments are parsed + for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { + argument->validate(); + } + + // Check each mutually exclusive group and make sure + // there are no constraint violations + for (const auto &group : m_mutually_exclusive_groups) { + auto mutex_argument_used{false}; + Argument *mutex_argument_it{nullptr}; + for (Argument *arg : group.m_elements) { + if (!mutex_argument_used && arg->m_is_used) { + mutex_argument_used = true; + mutex_argument_it = arg; + } else if (mutex_argument_used && arg->m_is_used) { + // Violation + throw std::runtime_error("Argument '" + arg->get_usage_full() + + "' not allowed with '" + + mutex_argument_it->get_usage_full() + "'"); + } + } + + if (!mutex_argument_used && group.m_required) { + // at least one argument from the group is + // required + std::string argument_names{}; + std::size_t i = 0; + std::size_t size = group.m_elements.size(); + for (Argument *arg : group.m_elements) { + if (i + 1 == size) { + // last + argument_names += std::string("'") + arg->get_usage_full() + std::string("' "); + } else { + argument_names += std::string("'") + arg->get_usage_full() + std::string("' or "); + } + i += 1; + } + throw std::runtime_error("One of the arguments " + argument_names + + "is required"); + } + } + } + + /* Call parse_known_args_internal - which does all the work + * Then, validate the parsed arguments + * This variant is used mainly for testing + * @throws std::runtime_error in case of any invalid argument + */ + std::vector + parse_known_args(const std::vector &arguments) { + auto unknown_arguments = parse_known_args_internal(arguments); + // Check if all arguments are parsed + for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { + argument->validate(); + } + return unknown_arguments; + } + + /* Main entry point for parsing command-line arguments using this + * ArgumentParser + * @throws std::runtime_error in case of any invalid argument + */ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) + void parse_args(int argc, const char *const argv[]) { + parse_args({argv, argv + argc}); + } + + /* Main entry point for parsing command-line arguments using this + * ArgumentParser + * @throws std::runtime_error in case of any invalid argument + */ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) + auto parse_known_args(int argc, const char *const argv[]) { + return parse_known_args({argv, argv + argc}); + } + + /* Getter for options with default values. + * @throws std::logic_error if parse_args() has not been previously called + * @throws std::logic_error if there is no such option + * @throws std::logic_error if the option has no value + * @throws std::bad_any_cast if the option is not of type T + */ + template T get(std::string_view arg_name) const { + if (!m_is_parsed) { + throw std::logic_error("Nothing parsed, no arguments are available."); + } + return (*this)[arg_name].get(); + } + + /* Getter for options without default values. + * @pre The option has no default value. + * @throws std::logic_error if there is no such option + * @throws std::bad_any_cast if the option is not of type T + */ + template + auto present(std::string_view arg_name) const -> std::optional { + return (*this)[arg_name].present(); + } + + /* Getter that returns true for user-supplied options. Returns false if not + * user-supplied, even with a default value. + */ + auto is_used(std::string_view arg_name) const { + return (*this)[arg_name].m_is_used; + } + + /* Getter that returns true if a subcommand is used. + */ + auto is_subcommand_used(std::string_view subcommand_name) const { + return m_subparser_used.at(std::string(subcommand_name)); + } + + /* Getter that returns true if a subcommand is used. + */ + auto is_subcommand_used(const ArgumentParser &subparser) const { + return is_subcommand_used(subparser.m_program_name); + } + + /* Indexing operator. Return a reference to an Argument object + * Used in conjunction with Argument.operator== e.g., parser["foo"] == true + * @throws std::logic_error in case of an invalid argument name + */ + Argument &operator[](std::string_view arg_name) const { + std::string name(arg_name); + auto it = m_argument_map.find(name); + if (it != m_argument_map.end()) { + return *(it->second); + } + if (!is_valid_prefix_char(arg_name.front())) { + const auto legal_prefix_char = get_any_valid_prefix_char(); + const auto prefix = std::string(1, legal_prefix_char); + + // "-" + arg_name + name = prefix + name; + it = m_argument_map.find(name); + if (it != m_argument_map.end()) { + return *(it->second); + } + // "--" + arg_name + name = prefix + name; + it = m_argument_map.find(name); + if (it != m_argument_map.end()) { + return *(it->second); + } + } + throw std::logic_error("No such argument: " + std::string(arg_name)); + } + + // Print help message + friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) + -> std::ostream & { + stream.setf(std::ios_base::left); + + auto longest_arg_length = parser.get_length_of_longest_argument(); + + stream << parser.usage() << "\n\n"; + + if (!parser.m_description.empty()) { + stream << parser.m_description << "\n\n"; + } + + const bool has_visible_positional_args = std::find_if( + parser.m_positional_arguments.begin(), + parser.m_positional_arguments.end(), + [](const auto &argument) { + return !argument.m_is_hidden; }) != + parser.m_positional_arguments.end(); + if (has_visible_positional_args) { + stream << "Positional arguments:\n"; + } + + for (const auto &argument : parser.m_positional_arguments) { + if (!argument.m_is_hidden) { + stream.width(static_cast(longest_arg_length)); + stream << argument; + } + } + + if (!parser.m_optional_arguments.empty()) { + stream << (!has_visible_positional_args ? "" : "\n") + << "Optional arguments:\n"; + } + + for (const auto &argument : parser.m_optional_arguments) { + if (argument.m_group_idx == 0 && !argument.m_is_hidden) { + stream.width(static_cast(longest_arg_length)); + stream << argument; + } + } + + for (size_t i_group = 0; i_group < parser.m_group_names.size(); ++i_group) { + stream << "\n" << parser.m_group_names[i_group] << " (detailed usage):\n"; + for (const auto &argument : parser.m_optional_arguments) { + if (argument.m_group_idx == i_group + 1 && !argument.m_is_hidden) { + stream.width(static_cast(longest_arg_length)); + stream << argument; + } + } + } + + bool has_visible_subcommands = std::any_of( + parser.m_subparser_map.begin(), parser.m_subparser_map.end(), + [](auto &p) { return !p.second->get().m_suppress; }); + + if (has_visible_subcommands) { + stream << (parser.m_positional_arguments.empty() + ? (parser.m_optional_arguments.empty() ? "" : "\n") + : "\n") + << "Subcommands:\n"; + for (const auto &[command, subparser] : parser.m_subparser_map) { + if (subparser->get().m_suppress) { + continue; + } + + stream << std::setw(2) << " "; + stream << std::setw(static_cast(longest_arg_length - 2)) + << command; + stream << " " << subparser->get().m_description << "\n"; + } + } + + if (!parser.m_epilog.empty()) { + stream << '\n'; + stream << parser.m_epilog << "\n\n"; + } + + return stream; + } + + // Format help message + auto help() const -> std::stringstream { + std::stringstream out; + out << *this; + return out; + } + + // Sets the maximum width for a line of the Usage message + ArgumentParser &set_usage_max_line_width(size_t w) { + this->m_usage_max_line_width = w; + return *this; + } + + // Asks to display arguments of mutually exclusive group on separate lines in + // the Usage message + ArgumentParser &set_usage_break_on_mutex() { + this->m_usage_break_on_mutex = true; + return *this; + } + + // Format usage part of help only + auto usage() const -> std::string { + std::stringstream stream; + + std::string curline("Usage: "); + curline += this->m_parser_path; + const bool multiline_usage = + this->m_usage_max_line_width < (std::numeric_limits::max)(); + const size_t indent_size = curline.size(); + + const auto deal_with_options_of_group = [&](std::size_t group_idx) { + bool found_options = false; + // Add any options inline here + const MutuallyExclusiveGroup *cur_mutex = nullptr; + int usage_newline_counter = -1; + for (const auto &argument : this->m_optional_arguments) { + if (argument.m_is_hidden) { + continue; + } + if (multiline_usage) { + if (argument.m_group_idx != group_idx) { + continue; + } + if (usage_newline_counter != argument.m_usage_newline_counter) { + if (usage_newline_counter >= 0) { + if (curline.size() > indent_size) { + stream << curline << std::endl; + curline = std::string(indent_size, ' '); + } + } + usage_newline_counter = argument.m_usage_newline_counter; + } + } + found_options = true; + const std::string arg_inline_usage = argument.get_inline_usage(); + const MutuallyExclusiveGroup *arg_mutex = + get_belonging_mutex(&argument); + if ((cur_mutex != nullptr) && (arg_mutex == nullptr)) { + curline += ']'; + if (this->m_usage_break_on_mutex) { + stream << curline << std::endl; + curline = std::string(indent_size, ' '); + } + } else if ((cur_mutex == nullptr) && (arg_mutex != nullptr)) { + if ((this->m_usage_break_on_mutex && curline.size() > indent_size) || + curline.size() + 3 + arg_inline_usage.size() > + this->m_usage_max_line_width) { + stream << curline << std::endl; + curline = std::string(indent_size, ' '); + } + curline += " ["; + } else if ((cur_mutex != nullptr) && (arg_mutex != nullptr)) { + if (cur_mutex != arg_mutex) { + curline += ']'; + if (this->m_usage_break_on_mutex || + curline.size() + 3 + arg_inline_usage.size() > + this->m_usage_max_line_width) { + stream << curline << std::endl; + curline = std::string(indent_size, ' '); + } + curline += " ["; + } else { + curline += '|'; + } + } + cur_mutex = arg_mutex; + if (curline.size() != indent_size && + curline.size() + 1 + arg_inline_usage.size() > + this->m_usage_max_line_width) { + stream << curline << std::endl; + curline = std::string(indent_size, ' '); + curline += " "; + } else if (cur_mutex == nullptr) { + curline += " "; + } + curline += arg_inline_usage; + } + if (cur_mutex != nullptr) { + curline += ']'; + } + return found_options; + }; + + const bool found_options = deal_with_options_of_group(0); + + if (found_options && multiline_usage && + !this->m_positional_arguments.empty()) { + stream << curline << std::endl; + curline = std::string(indent_size, ' '); + } + // Put positional arguments after the optionals + for (const auto &argument : this->m_positional_arguments) { + if (argument.m_is_hidden) { + continue; + } + const std::string pos_arg = !argument.m_metavar.empty() + ? argument.m_metavar + : argument.m_names.front(); + if (curline.size() + 1 + pos_arg.size() > this->m_usage_max_line_width) { + stream << curline << std::endl; + curline = std::string(indent_size, ' '); + } + curline += " "; + if (argument.m_num_args_range.get_min() == 0 && + !argument.m_num_args_range.is_right_bounded()) { + curline += "["; + curline += pos_arg; + curline += "]..."; + } else if (argument.m_num_args_range.get_min() == 1 && + !argument.m_num_args_range.is_right_bounded()) { + curline += pos_arg; + curline += "..."; + } else { + curline += pos_arg; + } + } + + if (multiline_usage) { + // Display options of other groups + for (std::size_t i = 0; i < m_group_names.size(); ++i) { + stream << curline << std::endl << std::endl; + stream << m_group_names[i] << ":" << std::endl; + curline = std::string(indent_size, ' '); + deal_with_options_of_group(i + 1); + } + } + + stream << curline; + + // Put subcommands after positional arguments + if (!m_subparser_map.empty()) { + stream << " {"; + std::size_t i{0}; + for (const auto &[command, subparser] : m_subparser_map) { + if (subparser->get().m_suppress) { + continue; + } + + if (i == 0) { + stream << command; + } else { + stream << "," << command; + } + ++i; + } + stream << "}"; + } + + return stream.str(); + } + + // Printing the one and only help message + // I've stuck with a simple message format, nothing fancy. + [[deprecated("Use cout << program; instead. See also help().")]] std::string + print_help() const { + auto out = help(); + std::cout << out.rdbuf(); + return out.str(); + } + + void add_subparser(ArgumentParser &parser) { + parser.m_parser_path = m_program_name + " " + parser.m_program_name; + auto it = m_subparsers.emplace(std::cend(m_subparsers), parser); + m_subparser_map.insert_or_assign(parser.m_program_name, it); + m_subparser_used.insert_or_assign(parser.m_program_name, false); + } + + void set_suppress(bool suppress) { m_suppress = suppress; } + +protected: + const MutuallyExclusiveGroup *get_belonging_mutex(const Argument *arg) const { + for (const auto &mutex : m_mutually_exclusive_groups) { + if (std::find(mutex.m_elements.begin(), mutex.m_elements.end(), arg) != + mutex.m_elements.end()) { + return &mutex; + } + } + return nullptr; + } + + bool is_valid_prefix_char(char c) const { + return m_prefix_chars.find(c) != std::string::npos; + } + + char get_any_valid_prefix_char() const { return m_prefix_chars[0]; } + + /* + * Pre-process this argument list. Anything starting with "--", that + * contains an =, where the prefix before the = has an entry in the + * options table, should be split. + */ + std::vector + preprocess_arguments(const std::vector &raw_arguments) const { + std::vector arguments{}; + for (const auto &arg : raw_arguments) { + + const auto argument_starts_with_prefix_chars = + [this](const std::string &a) -> bool { + if (!a.empty()) { + + const auto legal_prefix = [this](char c) -> bool { + return m_prefix_chars.find(c) != std::string::npos; + }; + + // Windows-style + // if '/' is a legal prefix char + // then allow single '/' followed by argument name, followed by an + // assign char, e.g., ':' e.g., 'test.exe /A:Foo' + const auto windows_style = legal_prefix('/'); + + if (windows_style) { + if (legal_prefix(a[0])) { + return true; + } + } else { + // Slash '/' is not a legal prefix char + // For all other characters, only support long arguments + // i.e., the argument must start with 2 prefix chars, e.g, + // '--foo' e,g, './test --foo=Bar -DARG=yes' + if (a.size() > 1) { + return (legal_prefix(a[0]) && legal_prefix(a[1])); + } + } + } + return false; + }; + + // Check that: + // - We don't have an argument named exactly this + // - The argument starts with a prefix char, e.g., "--" + // - The argument contains an assign char, e.g., "=" + auto assign_char_pos = arg.find_first_of(m_assign_chars); + + if (m_argument_map.find(arg) == m_argument_map.end() && + argument_starts_with_prefix_chars(arg) && + assign_char_pos != std::string::npos) { + // Get the name of the potential option, and check it exists + std::string opt_name = arg.substr(0, assign_char_pos); + if (m_argument_map.find(opt_name) != m_argument_map.end()) { + // This is the name of an option! Split it into two parts + arguments.push_back(std::move(opt_name)); + arguments.push_back(arg.substr(assign_char_pos + 1)); + continue; + } + } + // If we've fallen through to here, then it's a standard argument + arguments.push_back(arg); + } + return arguments; + } + + /* + * @throws std::runtime_error in case of any invalid argument + */ + void parse_args_internal(const std::vector &raw_arguments) { + auto arguments = preprocess_arguments(raw_arguments); + if (m_program_name.empty() && !arguments.empty()) { + m_program_name = arguments.front(); + } + auto end = std::end(arguments); + auto positional_argument_it = std::begin(m_positional_arguments); + for (auto it = std::next(std::begin(arguments)); it != end;) { + const auto ¤t_argument = *it; + if (Argument::is_positional(current_argument, m_prefix_chars)) { + if (positional_argument_it == std::end(m_positional_arguments)) { + + // Check sub-parsers + auto subparser_it = m_subparser_map.find(current_argument); + if (subparser_it != m_subparser_map.end()) { + + // build list of remaining args + const auto unprocessed_arguments = + std::vector(it, end); + + // invoke subparser + m_is_parsed = true; + m_subparser_used[current_argument] = true; + return subparser_it->second->get().parse_args( + unprocessed_arguments); + } + + if (m_positional_arguments.empty()) { + + // Ask the user if they argument they provided was a typo + // for some sub-parser, + // e.g., user provided `git totes` instead of `git notes` + if (!m_subparser_map.empty()) { + throw std::runtime_error( + "Failed to parse '" + current_argument + "', did you mean '" + + std::string{details::get_most_similar_string( + m_subparser_map, current_argument)} + + "'"); + } + + // Ask the user if they meant to use a specific optional argument + if (!m_optional_arguments.empty()) { + for (const auto &opt : m_optional_arguments) { + if (!opt.m_implicit_value.has_value()) { + // not a flag, requires a value + if (!opt.m_is_used) { + throw std::runtime_error( + "Zero positional arguments expected, did you mean " + + opt.get_usage_full()); + } + } + } + + throw std::runtime_error("Zero positional arguments expected"); + } else { + throw std::runtime_error("Zero positional arguments expected"); + } + } else { + throw std::runtime_error("Maximum number of positional arguments " + "exceeded, failed to parse '" + + current_argument + "'"); + } + } + auto argument = positional_argument_it++; + + // Deal with the situation of ... + if (argument->m_num_args_range.get_min() == 1 && + argument->m_num_args_range.get_max() == (std::numeric_limits::max)() && + positional_argument_it != std::end(m_positional_arguments) && + std::next(positional_argument_it) == std::end(m_positional_arguments) && + positional_argument_it->m_num_args_range.get_min() == 1 && + positional_argument_it->m_num_args_range.get_max() == 1 ) { + if (std::next(it) != end) { + positional_argument_it->consume(std::prev(end), end); + end = std::prev(end); + } else { + throw std::runtime_error("Missing " + positional_argument_it->m_names.front()); + } + } + + it = argument->consume(it, end); + continue; + } + + auto arg_map_it = m_argument_map.find(current_argument); + if (arg_map_it != m_argument_map.end()) { + auto argument = arg_map_it->second; + it = argument->consume(std::next(it), end, arg_map_it->first); + } else if (const auto &compound_arg = current_argument; + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { + ++it; + for (std::size_t j = 1; j < compound_arg.size(); j++) { + auto hypothetical_arg = std::string{'-', compound_arg[j]}; + auto arg_map_it2 = m_argument_map.find(hypothetical_arg); + if (arg_map_it2 != m_argument_map.end()) { + auto argument = arg_map_it2->second; + it = argument->consume(it, end, arg_map_it2->first); + } else { + throw std::runtime_error("Unknown argument: " + current_argument); + } + } + } else { + throw std::runtime_error("Unknown argument: " + current_argument); + } + } + m_is_parsed = true; + } + + /* + * Like parse_args_internal but collects unused args into a vector + */ + std::vector + parse_known_args_internal(const std::vector &raw_arguments) { + auto arguments = preprocess_arguments(raw_arguments); + + std::vector unknown_arguments{}; + + if (m_program_name.empty() && !arguments.empty()) { + m_program_name = arguments.front(); + } + auto end = std::end(arguments); + auto positional_argument_it = std::begin(m_positional_arguments); + for (auto it = std::next(std::begin(arguments)); it != end;) { + const auto ¤t_argument = *it; + if (Argument::is_positional(current_argument, m_prefix_chars)) { + if (positional_argument_it == std::end(m_positional_arguments)) { + + // Check sub-parsers + auto subparser_it = m_subparser_map.find(current_argument); + if (subparser_it != m_subparser_map.end()) { + + // build list of remaining args + const auto unprocessed_arguments = + std::vector(it, end); + + // invoke subparser + m_is_parsed = true; + m_subparser_used[current_argument] = true; + return subparser_it->second->get().parse_known_args_internal( + unprocessed_arguments); + } + + // save current argument as unknown and go to next argument + unknown_arguments.push_back(current_argument); + ++it; + } else { + // current argument is the value of a positional argument + // consume it + auto argument = positional_argument_it++; + it = argument->consume(it, end); + } + continue; + } + + auto arg_map_it = m_argument_map.find(current_argument); + if (arg_map_it != m_argument_map.end()) { + auto argument = arg_map_it->second; + it = argument->consume(std::next(it), end, arg_map_it->first); + } else if (const auto &compound_arg = current_argument; + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { + ++it; + for (std::size_t j = 1; j < compound_arg.size(); j++) { + auto hypothetical_arg = std::string{'-', compound_arg[j]}; + auto arg_map_it2 = m_argument_map.find(hypothetical_arg); + if (arg_map_it2 != m_argument_map.end()) { + auto argument = arg_map_it2->second; + it = argument->consume(it, end, arg_map_it2->first); + } else { + unknown_arguments.push_back(current_argument); + break; + } + } + } else { + // current argument is an optional-like argument that is unknown + // save it and move to next argument + unknown_arguments.push_back(current_argument); + ++it; + } + } + m_is_parsed = true; + return unknown_arguments; + } + + // Used by print_help. + std::size_t get_length_of_longest_argument() const { + if (m_argument_map.empty()) { + return 0; + } + std::size_t max_size = 0; + for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { + max_size = + std::max(max_size, argument->get_arguments_length()); + } + for ([[maybe_unused]] const auto &[command, unused] : m_subparser_map) { + max_size = std::max(max_size, command.size()); + } + return max_size; + } + + using argument_it = std::list::iterator; + using mutex_group_it = std::vector::iterator; + using argument_parser_it = + std::list>::iterator; + + void index_argument(argument_it it) { + for (const auto &name : std::as_const(it->m_names)) { + m_argument_map.insert_or_assign(name, it); + } + } + + std::string m_program_name; + std::string m_version; + std::string m_description; + std::string m_epilog; + bool m_exit_on_default_arguments = true; + std::string m_prefix_chars{"-"}; + std::string m_assign_chars{"="}; + bool m_is_parsed = false; + std::list m_positional_arguments; + std::list m_optional_arguments; + std::map m_argument_map; + std::string m_parser_path; + std::list> m_subparsers; + std::map m_subparser_map; + std::map m_subparser_used; + std::vector m_mutually_exclusive_groups; + bool m_suppress = false; + std::size_t m_usage_max_line_width = (std::numeric_limits::max)(); + bool m_usage_break_on_mutex = false; + int m_usage_newline_counter = 0; + std::vector m_group_names; +}; + +} // namespace argparse diff --git a/include/cigui/cigui.hpp b/include/cigui/cigui.hpp deleted file mode 100644 index cb07c78..0000000 --- a/include/cigui/cigui.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - - -#include -#include -#include -#include - -#include - -namespace cig { -constexpr unsigned int VERSION_MAJOR = CIGUI_VERSION_MAJOR; -constexpr unsigned int VERSION_MINOR = CIGUI_VERSION_MINOR; -constexpr unsigned int VERSION_PATCH = CIGUI_VERSION_PATCH; - -// Version string -constexpr const char *VERSION = CIGUI_VERSION; -} \ No newline at end of file diff --git a/include/cigui/config.h.in b/include/cigui/config.h.in deleted file mode 100644 index 7e46f48..0000000 --- a/include/cigui/config.h.in +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -// Version information -#define CIGUI_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ -#define CIGUI_VERSION_MINOR @PROJECT_VERSION_MINOR@ -#define CIGUI_VERSION_PATCH @PROJECT_VERSION_PATCH@ -#define CIGUI_VERSION "@PROJECT_VERSION@" - -// Export macros for DLL/shared library -#if defined(_MSC_VER) - #if defined(CIGUI_DLL) - #if defined(cigui_EXPORTS) // Set by CMake automatically - #define CIGUI_API __declspec(dllexport) - #define CIGUI_TEMPLATE - #else - #define CIGUI_API __declspec(dllimport) - #define CIGUI_TEMPLATE extern - #endif - #else - #define CIGUI_API - #define CIGUI_TEMPLATE - #endif -#else - #if defined(CIGUI_DLL) && defined(__GNUC__) && __GNUC__ >= 4 - #define CIGUI_API __attribute__ ((visibility ("default"))) - #define CIGUI_TEMPLATE - #else - #define CIGUI_API - #define CIGUI_TEMPLATE - #endif -#endif - -// For template classes, we use inline in headers -#define CIGUI_TEMPLATE_API - -// Special macro for template instantiations -#define CIGUI_TEMPLATE_INST extern template class CIGUI_API \ No newline at end of file diff --git a/include/cigui/core/Layout.hpp b/include/cigui/core/Layout.hpp deleted file mode 100644 index e35fde0..0000000 --- a/include/cigui/core/Layout.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include - - -namespace cig { -enum class LayoutSizes : uint8_t { - None = 0, - Min, - Max, - Fixed, -}; - -enum class LayoutAlignment : uint8_t { - None = 0, - Left, - Right, - Top, - Bottom, - Center, -}; - -enum class LayoutDirection : uint8_t { - None = 0, - LeftToRight, - RightToLeft, - TopToBottom, - BottomToTop, -}; - -enum class LayoutPosition : uint8_t { - None = 0, - Absolute, - Left, - Right, - Top, - Bottom, - Center, -}; - -struct Layout { - struct { - LayoutSizes rule = LayoutSizes::None; - vec2f minSize = {0.f, 0.f}; - vec2f maxSize = {0.f, 0.f}; - } size; - - struct { - LayoutAlignment rule = LayoutAlignment::None; - vec4f padding = {0.f, 0.f, 0.f, 0.f}; - vec4f margin = {0.f, 0.f, 0.f, 0.f}; - } alignment; - - struct { - LayoutDirection rule = LayoutDirection::None; - vec2f spacing = {0.f, 0.f}; - } direction; - - struct { - LayoutPosition rule = LayoutPosition::None; - vec2f position = {0.f, 0.f}; - } position; -}; -} \ No newline at end of file diff --git a/include/cigui/core/RenderCall.hpp b/include/cigui/core/RenderCall.hpp deleted file mode 100644 index f697cd4..0000000 --- a/include/cigui/core/RenderCall.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace cig { -class RenderCall { - std::shared_ptr drawable; - sf::RenderStates states; - -public: - explicit RenderCall(const sf::RenderStates &rstates, sf::Drawable *ptr) : drawable(ptr), states(rstates) { - if (!drawable) { - throw std::runtime_error("RenderCall::RenderCall(): Drawable is null"); - } - } - - void draw(sf::RenderTarget &target, const sf::RenderStates &rstates) const { - target.draw(*drawable, rstates); - } -}; -} \ No newline at end of file diff --git a/include/cigui/core/Renderer.hpp b/include/cigui/core/Renderer.hpp deleted file mode 100644 index 7597f1a..0000000 --- a/include/cigui/core/Renderer.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#pragma once - -#include -#include -#include -#include - - -namespace cig { - - class Renderer { - public: - explicit Renderer(View *_view) : view(_view) { view->draw(); } - - std::unique_ptr view; - - void update() const { - if (view->update()) - view->draw(); - } - - void render(sf::RenderTarget &target, const sf::RenderStates &states) const { - auto lambda = [&target, &states](const RenderCall& renderCall) { renderCall.draw(target, states); }; - view->renderCalls().iterate(lambda); - } - }; -} diff --git a/include/cigui/core/View.hpp b/include/cigui/core/View.hpp deleted file mode 100644 index 5617ebf..0000000 --- a/include/cigui/core/View.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - - -#include -#include -#include -#include - - -namespace cig -{ - class View { - protected: - List m_RenderCalls; - Layout m_Layout; - - public: - virtual ~View() = default; - - [[nodiscard]] const List &renderCalls() const { return m_RenderCalls; } - std::unique_ptr content; - - virtual bool update() { - if (content) - return content->update(); - return false; - } - - void draw() { - if (!m_RenderCalls.empty()) { - m_RenderCalls.clear(); - } - content = std::unique_ptr(body()); - if (!content) { - return; - } - content->draw(); - auto &contentRenderCalls = content->renderCalls(); - m_RenderCalls.expand(contentRenderCalls); - } - - virtual View *body() = 0; - }; -} \ No newline at end of file diff --git a/include/cigui/utils/List.hpp b/include/cigui/utils/List.hpp deleted file mode 100644 index 6a086d5..0000000 --- a/include/cigui/utils/List.hpp +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include -#include -namespace cig -{ - template - class List - { - std::unique_ptr m_Data; - size_t m_Size = 0; - size_t m_Capacity; - - protected: - void reserve(const size_t capacity); - - public: - explicit List(const size_t capacity = 3) : m_Capacity(capacity) { reserve(capacity); } - - void own(T* data, const size_t size) - { - m_Data = data; - m_Size = size; - } - - void copy(T* data, const size_t size) - { - m_Data = std::make_unique(size); - std::copy(data, data + size, m_Data.get()); - m_Size = size; - } - - [[nodiscard]] size_t size() const { return m_Size; } - - T& operator[](size_t index) { return m_Data.get()[index]; } - - const T& operator[](size_t index) const { return m_Data.get()[index]; } - - void need(const size_t additional_size) - { - if (m_Size + additional_size > m_Capacity) - reserve(m_Capacity + additional_size); - } - - [[nodiscard]] bool empty() const { return m_Size == 0; } - - void clear() { m_Size = 0; } - - void push_back(const T& value) - { - if (m_Size >= m_Capacity) - reserve(m_Capacity * growth_scalar + growth_summand); - m_Data.get()[m_Size++] = value; - } - - template - void emplace_back(Args&&... args) - { - if (m_Size >= m_Capacity) - reserve(m_Capacity * growth_scalar + growth_summand); - m_Data.get()[m_Size++] = T(std::forward(args)...); - } - - void expand(const List& other) - { - need(other.size()); - std::copy(other.m_Data.get(), other.m_Data.get() + other.size(), m_Data.get() + m_Size); - m_Size += other.size(); - } - - template - void iterate(Lambda&& lambda) const - { - for (size_t i = 0; i < m_Size; i++) - lambda(m_Data.get()[i]); - } - }; -} -#include \ No newline at end of file diff --git a/include/cigui/utils/List.inl b/include/cigui/utils/List.inl deleted file mode 100644 index bf937bf..0000000 --- a/include/cigui/utils/List.inl +++ /dev/null @@ -1,22 +0,0 @@ - -#define __LIST_FUNC_DEFINE__(rtt) \ - template \ - rtt List - - -namespace cig -{ - __LIST_FUNC_DEFINE__(void)::reserve(const size_t capacity) - { - if (!m_Data) - { - m_Data = std::unique_ptr(static_cast(calloc(capacity, sizeof(T)))); - m_Capacity = capacity; - return; - } - std::unique_ptr newData(static_cast(calloc(capacity, sizeof(T)))); - std::copy(m_Data.get(), m_Data.get() + m_Size, newData.get()); - m_Data = std::move(newData); - m_Capacity = capacity; - } -} diff --git a/include/cigui/utils/Vectors.hpp b/include/cigui/utils/Vectors.hpp deleted file mode 100644 index c44d461..0000000 --- a/include/cigui/utils/Vectors.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include - -#define TYPEDEF_VECTOR(NAME, T, N, s) typedef NAME vec##N##s; - -#define TYPEDEF_VECTORS(NAME, N) \ - typedef NAME vec##N; \ - TYPEDEF_VECTOR(NAME, float, N, f) \ - TYPEDEF_VECTOR(NAME, double, N, d) \ - TYPEDEF_VECTOR(NAME, long double, N, ld) \ - TYPEDEF_VECTOR(NAME, size_t, N, sz) \ - TYPEDEF_VECTOR(NAME, int, N, i) \ - TYPEDEF_VECTOR(NAME, unsigned int, N, u) \ - TYPEDEF_VECTOR(NAME, short, N, s) \ - TYPEDEF_VECTOR(NAME, unsigned short, N, us) \ - TYPEDEF_VECTOR(NAME, long, N, l) \ - TYPEDEF_VECTOR(NAME, unsigned long, N, ul) \ - TYPEDEF_VECTOR(NAME, long long, N, ll) \ - TYPEDEF_VECTOR(NAME, unsigned long long, N, ull) - -#if defined(__GNUC__) || defined(__clang__) - #define UNNAMED_STRUCT __extension__ struct -#else - #defien UNNAMED_STRUCT struct -#endif - - -namespace cig { -template -union Vector2 { - UNNAMED_STRUCT { - T x, y; - }; - UNNAMED_STRUCT { - T a, b; - }; -}; - -TYPEDEF_VECTORS(Vector2, 2) - -template -union Vector3 { - UNNAMED_STRUCT { - T x, y, z; - }; - UNNAMED_STRUCT { - T r, g, b; - }; -}; -TYPEDEF_VECTORS(Vector3, 3) - -template -union Vector4 { - UNNAMED_STRUCT { - T x, y, z, w; - }; - UNNAMED_STRUCT { - T r, g, b, a; - }; - UNNAMED_STRUCT { - T left, top, right, bottom; - }; -}; -TYPEDEF_VECTORS(Vector4, 4) -} diff --git a/include/cigui/views/Rectangle.hpp b/include/cigui/views/Rectangle.hpp deleted file mode 100644 index c21e9eb..0000000 --- a/include/cigui/views/Rectangle.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include - -namespace cig { - struct Rectangle : View { - sf::Color m_Color; - sf::Color m_BorderColor; - float m_BorderThickness = 0; - - Rectangle *setBorderColor(const sf::Color &color) { - m_BorderColor = color; - return this; - } - Rectangle *setBorderThickness(float thickness) { - m_BorderThickness = thickness; - return this; - } - Rectangle *setColor(const sf::Color &color) { - m_Color = color; - return this; - } - Rectangle *setSize(const vec2f &size) { - m_Layout.size.minSize = size; - return this; - } - - View *body() override { - auto m_Shape = new sf::RectangleShape(sf::Vector2f{m_Layout.size.minSize.x, m_Layout.size.minSize.y}); - m_Shape->setFillColor(m_Color); - if (m_BorderThickness > 0) { - m_Shape->setOutlineThickness(m_BorderThickness); - m_Shape->setOutlineColor(m_BorderColor); - } - m_RenderCalls.emplace_back(sf::RenderStates(), m_Shape); - return nullptr; - } - - explicit Rectangle(const vec2f &size) { - m_Layout.size.minSize = size; - } - }; -} \ No newline at end of file diff --git a/include/cigui/views/views.hpp b/include/cigui/views/views.hpp deleted file mode 100644 index 41fd855..0000000 --- a/include/cigui/views/views.hpp +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#include \ No newline at end of file diff --git a/src/core/RenderCall.cpp b/src/core/RenderCall.cpp deleted file mode 100644 index 5f28270..0000000 --- a/src/core/RenderCall.cpp +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/Full/General/src/main.cpp b/src/main.cpp similarity index 88% rename from examples/Full/General/src/main.cpp rename to src/main.cpp index 6ee210c..cd3fc9a 100644 --- a/examples/Full/General/src/main.cpp +++ b/src/main.cpp @@ -4,13 +4,12 @@ #include -#include -#include +#include // WIP - not working yet // Layout unsupported struct HStack final : cig::View { - using Stack = cig::List; + using Stack = cig::utils::List; Stack views; void append(View* view) { views.push_back(view); } @@ -22,8 +21,8 @@ struct HStack final : cig::View { } View* body() override { - views.iterate<>([this](View*& view) { view->draw(); }); - views.iterate([this](View*& view) { + views.iterate([this](View* view) { view->draw(); }); + views.iterate([this](const View* view) { this->m_RenderCalls.expand(view->renderCalls()); }); return nullptr; diff --git a/src/utils/List.cpp b/src/utils/List.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/Vectors.cpp b/src/utils/Vectors.cpp deleted file mode 100644 index 8c3669b..0000000 --- a/src/utils/Vectors.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include - -#define VECTOR_TEMPLATE_INSTANTIATION(N, T) \ - template union Vector##N; - -#define VECTOR_TEMPLATE_IMPLEMENTATION(N) \ - VECTOR_TEMPLATE_INSTANTIATION(N, int) \ - VECTOR_TEMPLATE_INSTANTIATION(N, float) \ - VECTOR_TEMPLATE_INSTANTIATION(N, double)\ - VECTOR_TEMPLATE_INSTANTIATION(N, unsigned int) \ - VECTOR_TEMPLATE_INSTANTIATION(N, unsigned long) \ - VECTOR_TEMPLATE_INSTANTIATION(N, long) \ - VECTOR_TEMPLATE_INSTANTIATION(N, unsigned long long) \ - VECTOR_TEMPLATE_INSTANTIATION(N, long long) \ - VECTOR_TEMPLATE_INSTANTIATION(N, short) \ - VECTOR_TEMPLATE_INSTANTIATION(N, unsigned short) \ - VECTOR_TEMPLATE_INSTANTIATION(N, bool) - -namespace cig { - VECTOR_TEMPLATE_IMPLEMENTATION(2) - VECTOR_TEMPLATE_IMPLEMENTATION(3) - VECTOR_TEMPLATE_IMPLEMENTATION(4) -} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt deleted file mode 100644 index 57106f1..0000000 --- a/tests/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -cmake_minimum_required(VERSION 3.14) - -# Enable testing -enable_testing() - -# Enable code coverage -if(CMAKE_COMPILER_IS_GNUCXX) - option(CIGUI_CODE_COVERAGE "Enable code coverage reporting" OFF) - if(CIGUI_CODE_COVERAGE) - target_compile_options(cigui_tests PRIVATE --coverage) - target_link_options(cigui_tests PRIVATE --coverage) - endif() -endif() - -# Add GoogleTest (fetched via CPM) -CPMAddPackage( - NAME GTest - GITHUB_REPOSITORY google/googletest - VERSION 1.16.0 - OPTIONS - "INSTALL_GTEST OFF" - "gtest_force_shared_crt ON" -) - -# Function to easily add test files -function(add_cigui_test TEST_NAME) - add_executable(${TEST_NAME} ${ARGN}) - target_link_libraries(${TEST_NAME} PRIVATE cigui gtest gtest_main gmock) - target_include_directories(${TEST_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/src) - - # Add test to CTest - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) - - # Copy DLLs on Windows when using shared libraries - if(WIN32 AND CIGUI_BUILD_SHARED) - add_custom_command(TARGET ${TEST_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - $ - $ - $ - ) - endif() -endfunction() - -# Main test executable -add_executable( - cigui_tests - test_main.cpp -) - -# Link against the library and test framework -target_link_libraries( - cigui_tests - PRIVATE - cigui - gtest - gmock -) - -# Include private headers for white-box testing -target_include_directories( - cigui_tests - PRIVATE - ${CMAKE_SOURCE_DIR}/src -) - -# Add main test to CTest -add_test(NAME cigui_tests COMMAND cigui_tests) - -# Copy DLLs on Windows when using shared libraries -if(WIN32 AND CIGUI_BUILD_SHARED) - add_custom_command(TARGET cigui_tests POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - $ - $ - $ - ) -endif() - -# Optional: Individual test executables -# Uncomment to build separate test executables in addition to the main one -# add_cigui_test(widget_test widgets/widget_test.cpp) -# add_cigui_test(button_test widgets/button_test.cpp) -# add_cigui_test(window_test core/window_test.cpp) diff --git a/tests/test_main.cpp b/tests/test_main.cpp deleted file mode 100644 index 004a26b..0000000 --- a/tests/test_main.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include -#include -#include - -// This main can be used to customize test execution -int main(int argc, char** argv) { - std::cout << "Running CIGUI Library Tests\n"; - - // Initialize Google Test - ::testing::InitGoogleTest(&argc, argv); - - // Run the tests and return the result - return RUN_ALL_TESTS(); -}