diff --git a/.clang-format b/.clang-format index e34dcd1..09cb3cf 100644 --- a/.clang-format +++ b/.clang-format @@ -13,10 +13,11 @@ AlignConsecutiveShortCaseStatements: AlignOperands: DontAlign AllowShortBlocksOnASingleLine: Always AllowShortCaseLabelsOnASingleLine: true -AllowShortFunctionsOnASingleLine: All +AllowShortFunctionsOnASingleLine: Empty AllowShortLoopsOnASingleLine: true BinPackArguments: false -ColumnLimit: 120 +BreakBeforeBraces: Attach +ColumnLimit: 0 ConstructorInitializerIndentWidth: 2 ContinuationIndentWidth: 2 Cpp11BracedListStyle: false diff --git a/include/argparse.hpp b/include/argparse.hpp new file mode 100644 index 0000000..2961f79 --- /dev/null +++ b/include/argparse.hpp @@ -0,0 +1,2582 @@ +#pragma once + +/* + * __ _ _ __ __ _ _ __ __ _ _ __ ___ ___ + * / _` | '__/ _` | '_ \ / _` | '__/ __|/ _ \ 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. + */ + +#include +#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 + #include +#endif + +#include "src/util/defs.hpp" +#include "src/util/types.hpp" + +#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 + +// ReSharper disable CppTemplateParameterNeverUsed, CppDFATimeOver +// NOLINTBEGIN(readability-identifier-naming, readability-identifier-length) +namespace argparse { + using namespace util::types; + + 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().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() << std::declval())>> : std::true_type {}; + + template + inline constexpr bool IsStreamable = HasStreamableTraits::value; + + constexpr usize repr_max_container_size = 5; + + template + concept Formattable = requires(const T& t, std::basic_format_context ctx) { + std::formatter, CharT>().format(t, ctx); + }; + + template + static auto repr(const T& val) -> String { + if constexpr (std::is_same_v) + return val ? "true" : "false"; + else if constexpr (std::is_convertible_v) + return std::format("\"{}\"", String { StringView { val } }); + else if constexpr (IsContainer) { + String result = "{"; + + const auto size = val.size(); + + if (size > 0) { + bool first = true; + + auto transformed_view = val | std::views::transform([](const auto& elem) { return details::repr(elem); }); + + if (size <= repr_max_container_size) { + for (const String& elem_repr : transformed_view) { + if (!first) + result += " "; + + result += elem_repr; + first = false; + } + } else { + usize count = 0; + + for (const String& elem_repr : transformed_view | std::views::take(repr_max_container_size - 1)) { + if (!first) + result += " "; + + result += elem_repr; + first = false; + count++; + } + + result += "... "; + + result += details::repr(*std::prev(val.end())); + } + } + + result += "}"; + return result; + } else if constexpr (Formattable) + return std::format("{}", val); + else if constexpr (IsStreamable) { + std::stringstream out; + out << val; + return out.str(); + } else + return ""; + } + + constexpr i32 radix_2 = 2; + constexpr i32 radix_8 = 8; + constexpr i32 radix_10 = 10; + constexpr i32 radix_16 = 16; + + template + constexpr fn apply_plus_one_impl(F&& f, Tuple&& t, Extra&& x, std::index_sequence /*unused*/) -> decltype(auto) { + return std::invoke(std::forward(f), std::get(std::forward(t))..., std::forward(x)); + } + + template + constexpr fn apply_plus_one(F&& f, Tuple&& t, Extra&& x) -> decltype(auto) { + return details::apply_plus_one_impl( + std::forward(f), std::forward(t), std::forward(x), std::make_index_sequence>> {} + ); + } + + constexpr fn pointer_range(const StringView s) noexcept -> std::tuple { + return { s.data(), s.data() + s.size() }; + } + + template + constexpr fn starts_with(std::basic_string_view prefix, std::basic_string_view s) noexcept -> bool { + return s.substr(0, prefix.size()) == prefix; + } + + enum class chars_format : u8 { + scientific = 0xf1, + fixed = 0xf2, + hex = 0xf4, + binary = 0xf8, + general = fixed | scientific + }; + + struct ConsumeBinaryPrefixResult { + bool is_binary; + StringView rest; + }; + + constexpr fn consume_binary_prefix(StringView s) -> ConsumeBinaryPrefixResult { + if (starts_with(StringView { "0b" }, s) || + starts_with(StringView { "0B" }, s)) { + s.remove_prefix(2); + return { .is_binary = true, .rest = s }; + } + + return { .is_binary = false, .rest = s }; + } + + struct ConsumeHexPrefixResult { + bool is_hexadecimal; + StringView rest; + }; + + using namespace std::literals; + + constexpr fn consume_hex_prefix(StringView s) -> ConsumeHexPrefixResult { + if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { + s.remove_prefix(2); + return { .is_hexadecimal = true, .rest = s }; + } + + return { .is_hexadecimal = false, .rest = s }; + } + + template + fn do_from_chars(const StringView 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 '" + String(s) + "' does not match to the end" }; + } + + if (ec == std::errc::invalid_argument) + throw std::invalid_argument { "pattern '" + String(s) + "' not found" }; + + if (ec == std::errc::result_out_of_range) + throw std::range_error { "'" + String(s) + "' not representable" }; + + return x; // unreachable + } + + template + struct parse_number { + static fn operator()(const StringView s)->T { + return do_from_chars(s); + } + }; + + template + struct parse_number { + static fn operator()(const StringView 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 { + static fn operator()(const StringView 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 '" + String(s) + "' as hexadecimal: " + err.what()); + } catch (const std::range_error& err) { + throw std::range_error("Failed to parse '" + 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 '" + String(s) + "' as hexadecimal: " + err.what()); + } catch (const std::range_error& err) { + throw std::range_error("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what()); + } + + throw std::invalid_argument { "pattern '" + String(s) + + "' not identified as hexadecimal" }; + } + }; + + template + struct parse_number { + static fn operator()(const StringView 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 '" + String(s) + "' as hexadecimal: " + err.what()); + } catch (const std::range_error& err) { + throw std::range_error("Failed to parse '" + String(s) + "' as hexadecimal: " + err.what()); + } + + if (auto [ok_binary, rest_binary] = consume_binary_prefix(s); ok_binary) + try { + return do_from_chars(rest_binary); + } catch (const std::invalid_argument& err) { + throw std::invalid_argument("Failed to parse '" + String(s) + "' as binary: " + err.what()); + } catch (const std::range_error& err) { + throw std::range_error("Failed to parse '" + 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 '" + String(s) + "' as octal: " + err.what()); + } catch (const std::range_error& err) { + throw std::range_error("Failed to parse '" + 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 '" + String(s) + "' as decimal integer: " + err.what()); + } catch (const std::range_error& err) { + throw std::range_error("Failed to parse '" + String(s) + "' as decimal integer: " + err.what()); + } + } + }; + + template + inline constexpr std::nullptr_t 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; + + template + fn do_strtod(const String& 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 = nullptr; + + 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 { + fn operator()(const String& s)->T { + if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) + throw std::invalid_argument { + "chars_format::general does not parse hexfloat" + }; + + if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin) + 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 { + fn operator()(const String& s)->T { + if (auto [is_hex, rest] = consume_hex_prefix(s); !is_hex) + throw std::invalid_argument { "chars_format::hex parses hexfloat" }; + + if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin) + 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 { + fn operator()(const String& s)->T { + if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) + throw std::invalid_argument { + "chars_format::binary does not parse hexfloat" + }; + + if (auto [is_bin, rest] = consume_binary_prefix(s); !is_bin) + throw std::invalid_argument { "chars_format::binary parses binfloat" }; + + return do_strtod(s); + } + }; + + template + struct parse_number { + fn operator()(const String& s)->T { + if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) + throw std::invalid_argument { + "chars_format::scientific does not parse hexfloat" + }; + + if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) + throw std::invalid_argument { + "chars_format::scientific does not parse binfloat" + }; + + if (s.find_first_of("eE") == 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 { + fn operator()(const String& s)->T { + if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) + throw std::invalid_argument { + "chars_format::fixed does not parse hexfloat" + }; + + if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) + throw std::invalid_argument { + "chars_format::fixed does not parse binfloat" + }; + + if (s.find_first_of("eE") != 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 + concept ToStringConvertible = std::convertible_to || + std::convertible_to || + requires(const T& t) { std::format("{}", t); }; + + template + fn join(StrIt first, StrIt last, const String& separator) -> String { + 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 + // ReSharper disable CppFunctionIsNotImplemented + static fn test(int) -> decltype(std::to_string(std::declval()), std::true_type {}); + + template + static fn test(...) -> std::false_type; + // ReSharper restore CppFunctionIsNotImplemented + + static constexpr bool value = decltype(test(0))::value; + }; + + template + struct IsChoiceTypeSupported { + using CleanType = std::decay_t; + static const bool value = std::is_integral_v || + std::is_same_v || + std::is_same_v || + std::is_same_v; + }; + + template + fn get_levenshtein_distance(const StringType& s1, const StringType& s2) -> usize { + Vec> dp( + s1.size() + 1, Vec(s2.size() + 1, 0) + ); + + for (usize i = 0; i <= s1.size(); ++i) { + for (usize 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 + fn get_most_similar_string(const Map& map, const String& input) -> String { + String most_similar {}; + usize min_distance = (std::numeric_limits::max)(); + + for (const auto& entry : map) + if (const usize distance = get_levenshtein_distance(entry.first, input); distance < min_distance) { + min_distance = distance; + most_similar = entry.first; + } + + return most_similar; + } + + } // namespace details + + enum class nargs_pattern : u8 { + optional, + any, + at_least_one, + }; + + enum class default_arguments : u8 { + none = 0, + help = 1, + version = 2, + all = help | version, + }; + + inline fn operator&(const default_arguments& a, const default_arguments& b)->default_arguments { + return static_cast( + std::to_underlying(a) & + std::to_underlying(b) + ); + } + + class ArgumentParser; + + class Argument { + friend class ArgumentParser; + friend fn operator<<(std::ostream& stream, const ArgumentParser& parser) + ->std::ostream&; + + template + explicit Argument(const StringView prefix_chars, std::array&& a, std::index_sequence /*unused*/) // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) + : 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(StringView prefix_chars, std::array&& a) + : Argument(prefix_chars, std::move(a), std::make_index_sequence {}) {} + + fn help(String help_text) -> Argument& { + m_help = std::move(help_text); + return *this; + } + + fn metavar(String metavar) -> Argument& { + m_metavar = std::move(metavar); + return *this; + } + + template + fn default_value(T&& value) -> Argument& { + 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 = String { StringView { 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; + } + + fn default_value(const char* value) -> Argument& { + return default_value(String(value)); + } + + fn required() -> Argument& { + m_is_required = true; + return *this; + } + + fn implicit_value(std::any value) -> Argument& { + 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) + fn flag() -> Argument& { + default_value(false); + implicit_value(true); + return *this; + } + + template + fn action(F&& callable, Args&&... bound_args) + -> Argument& requires(std::is_invocable_v) { + using action_type = std::conditional_t>, 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)...)](const String& opt) mutable { + return details::apply_plus_one(f, tup, opt); + } + ); + + return *this; + } + + fn store_into(bool& var) + ->Argument& { + 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 String& /*unused*/) { + var = true; + return var; + }); + + return *this; + } + + template + fn store_into(T& var) -> Argument& requires(std::is_integral_v) { + 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 + fn store_into(T& var)->Argument& requires(std::is_floating_point_v) { + 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; + } + + fn store_into(String& var) + ->Argument& { + if (m_default_value.has_value()) + var = std::any_cast(m_default_value); + + action([&var](const String& s) { + var = s; + return var; + }); + + return *this; + } + + fn store_into(std::filesystem::path& var) -> Argument& { + if (m_default_value.has_value()) + var = std::any_cast(m_default_value); + + action([&var](const String& s) { var = s; }); + + return *this; + } + + fn store_into(Vec& var) -> Argument& { + if (m_default_value.has_value()) + var = std::any_cast>(m_default_value); + + action([this, &var](const String& s) { + if (!m_is_used) + var.clear(); + + m_is_used = true; + var.push_back(s); + return var; + }); + + return *this; + } + + fn store_into(Vec& var) -> Argument& { + if (m_default_value.has_value()) + var = std::any_cast>(m_default_value); + + action([this, &var](const String& s) { + if (!m_is_used) + var.clear(); + + m_is_used = true; + var.push_back(details::parse_number()(s)); + return var; + }); + + return *this; + } + + fn store_into(std::set& var) -> Argument& { + if (m_default_value.has_value()) + var = std::any_cast>(m_default_value); + + action([this, &var](const String& s) { + if (!m_is_used) + var.clear(); + + m_is_used = true; + var.insert(s); + return var; + }); + + return *this; + } + + fn store_into(std::set& var) -> Argument& { + if (m_default_value.has_value()) + var = std::any_cast>(m_default_value); + + action([this, &var](const String& s) { + if (!m_is_used) + var.clear(); + + m_is_used = true; + var.insert(details::parse_number()(s)); + return var; + }); + + return *this; + } + + fn append() -> Argument& { + m_is_repeatable = true; + return *this; + } + + // Cause the argument to be invisible in usage and help + fn hidden() -> Argument& { + m_is_hidden = true; + return *this; + } + + template + fn scan() -> Argument& requires(std::is_arithmetic_v) { + static_assert(!(std::is_const_v || std::is_volatile_v), "T should not be cv-qualified"); + + fn is_one_of = [](char c, auto... x) constexpr { + return ((c == x) || ...); + }; + + if constexpr (Shape == 'd' && std::is_integral_v) + action(details::parse_number()); + else if constexpr (Shape == 'i' && std::is_integral_v) + action(details::parse_number()); + else if constexpr (Shape == 'u' && (std::is_integral_v && std::is_unsigned_v)) + action(details::parse_number()); + else if constexpr (Shape == 'b' && (std::is_integral_v && std::is_unsigned_v)) + action(details::parse_number()); + else if constexpr (Shape == 'o' && (std::is_integral_v && std::is_unsigned_v)) + action(details::parse_number()); + else if constexpr (is_one_of(Shape, 'x', 'X') && (std::is_integral_v && std::is_unsigned_v)) + 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(false, "No scan specification for T"); + + return *this; + } + + fn nargs(const usize num_args) + ->Argument& { + m_num_args_range = NArgsRange { num_args, num_args }; + return *this; + } + + fn nargs(const usize num_args_min, const usize num_args_max) -> Argument& { + m_num_args_range = NArgsRange { num_args_min, num_args_max }; + return *this; + } + + fn nargs(const nargs_pattern pattern) -> Argument& { + 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; + } + + fn remaining() -> Argument& { + m_accepts_optional_like_value = true; + return nargs(nargs_pattern::any); + } + + template + fn add_choice(T&& choice) -> void { + 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 = Vec {}; + + if constexpr (std::is_convertible_v) + m_choices.value().emplace_back(StringView { std::forward(choice) }); + else if constexpr (details::can_invoke_to_string::value) + m_choices.value().push_back(std::to_string(std::forward(choice))); + } + + fn choices() -> Argument& { + if (!m_choices.has_value()) + throw std::runtime_error("Zero choices provided"); + + return *this; + } + + template + fn choices(T&& first, U&&... rest) -> Argument& { + add_choice(std::forward(first)); + choices(std::forward(rest)...); + return *this; + } + + fn find_default_value_in_choices_or_throw() const -> void { + assert(m_choices.has_value()); + const Vec& choices = m_choices.value(); + + if (m_default_value.has_value()) { + if (std::ranges::find(choices, m_default_value_str) == choices.end()) { + // provided arg not in list of allowed choices + // report error + + const String choices_as_csv = + std::accumulate(choices.begin(), choices.end(), String(), [](const String& a, const String& b) { + return a + (a.empty() ? "" : ", ") + b; + }); + + throw std::runtime_error( + String { "Invalid default value " } + m_default_value_repr + + " - allowed options: {" + choices_as_csv + "}" + ); + } + } + } + + template + [[nodiscard]] fn is_value_in_choices(Iterator option_it) const -> bool { + assert(m_choices.has_value()); + const Vec& choices = m_choices.value(); + + return (std::find(choices.begin(), choices.end(), *option_it) != choices.end()); + } + + template + fn throw_invalid_arguments_error(Iterator option_it) const -> void { + assert(m_choices.has_value()); + const Vec& choices = m_choices.value(); + + const String choices_as_csv = std::accumulate( + choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) { + return option_a + (option_a.empty() ? "" : ", ") + option_b; + } + ); + + throw std::runtime_error(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 + fn consume(Iterator start, Iterator end, const StringView used_name = {}, const bool dry_run = false) -> Iterator { + if (!m_is_repeatable && m_is_used) + throw std::runtime_error( + String("Duplicate argument ").append(used_name) + ); + + m_used_name = used_name; + + usize 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 usize num_args_max = (m_choices.has_value()) ? passed_options : m_num_args_range.get_max(); + const usize num_args_min = m_num_args_range.get_min(); + + 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 (auto dist = static_cast(std::distance(start, end)); dist >= 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, [this](T&& PH1) { return is_optional(std::forward(PH1), m_prefix_chars); } + ); + + dist = static_cast(std::distance(start, end)); + + if (dist < num_args_min) + throw std::runtime_error("Too few arguments for '" + String(m_used_name) + "'."); + } + + struct ActionApply { + fn operator()(valued_action& f)->void { + std::transform(first, last, std::back_inserter(self.m_values), f); + } + + fn operator()(void_action& f)->void { + 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 (std::variant& 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 '" + String(m_used_name) + "'."); + } + + /* + * @throws std::runtime_error if argument values are not valid + */ + fn validate() const -> void { + 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(); + } + + [[nodiscard]] fn get_names_csv(const char separator = ',') const -> String { + return std::accumulate( + m_names.begin(), m_names.end(), String { "" }, [&](const String& result, const String& name) { + return result.empty() ? name : result + separator + name; + } + ); + } + + [[nodiscard]] fn get_usage_full() const -> String { + std::stringstream usage; + + usage << get_names_csv('/'); + const 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(); + } + + [[nodiscard]] fn get_inline_usage() const -> String { + std::stringstream usage; + + // Find the longest variant to show in the usage string + String longest_name = m_names.front(); + for (const String& s : m_names) + if (s.size() > longest_name.size()) + longest_name = s; + + if (!m_is_required) + usage << "["; + + usage << longest_name; + const 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.contains("> <")) + usage << "..."; + } + + if (!m_is_required) + usage << "]"; + + if (m_is_repeatable) + usage << "..."; + + return usage.str(); + } + + [[nodiscard]] fn get_arguments_length() const -> usize { + const usize names_size = std::accumulate( + std::begin(m_names), std::end(m_names), static_cast(0), [](const u32& sum, const String& 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) + usize 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 fn operator<<(std::ostream& stream, const Argument& argument)->std::ostream& { + std::stringstream name_stream; + name_stream << " "; // indent + // + if (argparse::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 }) || + (argument.m_num_args_range.get_min() == argument.m_num_args_range.get_max() && + argument.m_metavar.contains("> <")))) { + name_stream << " " << argument.m_metavar; + } + } + + // align multiline help message + const std::streamsize stream_width = stream.width(); + const String name_padding = String(name_stream.str().size(), ' '); + + auto pos = String::size_type {}; + auto prev = String::size_type {}; + + bool first_line = true; + const char* hspace = " "; // minimal space between name and help message + + stream << name_stream.str(); + + const StringView help_view(argument.m_help); + + while ((pos = argument.m_help.find('\n', prev)) != String::npos) { + const StringView 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 if (const StringView leftover = help_view.substr(prev, argument.m_help.size() - prev); !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 + fn operator!=(const T& rhs) const->bool { + return !(*this == rhs); + } + + /* + * Compare to an argument value of known type + * @throws std::logic_error in case of incompatible types + */ + template + fn operator==(const T& rhs) const->bool { + 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 fn is_positional(StringView name, const StringView prefix_chars) -> bool { + const int first = lookahead(name); + + if (first == eof) + return true; + + if (prefix_chars.contains(static_cast(first))) { + name.remove_prefix(1); + + if (name.empty()) + return true; + + return is_decimal_literal(name); + } + + return true; + } + + private: + class NArgsRange { + usize m_min; + usize m_max; + + public: + NArgsRange(const usize minimum, const usize maximum) + : m_min(minimum), m_max(maximum) { + if (minimum > maximum) + throw std::logic_error("Range of number of arguments is invalid"); + } + + [[nodiscard]] fn contains(const usize value) const -> bool { + return value >= m_min && value <= m_max; + } + + [[nodiscard]] fn is_exact() const -> bool { + return m_min == m_max; + } + + [[nodiscard]] fn is_right_bounded() const -> bool { + return m_max < (std::numeric_limits::max)(); + } + + [[nodiscard]] fn get_min() const -> usize { + return m_min; + } + + [[nodiscard]] fn get_max() const -> usize { + return m_max; + } + + // Print help message + friend fn 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; + } + + fn operator==(const NArgsRange& rhs) const->bool { + return rhs.m_min == m_min && rhs.m_max == m_max; + } + + fn operator!=(const NArgsRange& rhs) const->bool { + return !(*this == rhs); + } + }; + + fn throw_nargs_range_validation_error() const -> void { + 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()); + } + + fn throw_required_arg_not_used_error() const -> void { + std::stringstream stream; + stream << m_names.front() << ": required."; + throw std::runtime_error(stream.str()); + } + + fn throw_required_arg_no_value_provided_error() const -> void { + 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 fn lookahead(const StringView sview) -> int { + if (sview.empty()) + return eof; + + return static_cast(sview[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 + * '+' '-' + */ + // NOLINTBEGIN(cppcoreguidelines-avoid-goto) + static fn is_decimal_literal(StringView s) -> bool { + fn is_digit = [](auto c) constexpr -> bool { + return c >= '0' && c <= '9'; + }; + + // precondition: we have consumed or will consume at least one digit + fn consume_digits = [=](StringView sd) -> StringView { + const char* const it = std::ranges::find_if_not(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; + + 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); + default: + break; + } + + if (is_digit(lookahead(s))) { + s = consume_digits(s); + return s.empty(); + } + + return false; + } + // NOLINTEND(cppcoreguidelines-avoid-goto) + + static fn is_optional(const StringView name, const StringView prefix_chars) -> bool { + return !is_positional(name, prefix_chars); + } + + /* + * Get argument value given a type + * @throws std::logic_error in case of incompatible types + */ + template + fn get() const -> T { + 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 + fn present() const -> Option { + 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 fn any_cast_container(const Vec& 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; + } + + fn set_usage_newline_counter(const int i) -> void { + m_usage_newline_counter = i; + } + + fn set_group_idx(const usize i) -> void { + m_group_idx = i; + } + + Vec m_names; + StringView m_used_name; + String m_help; + String m_metavar; + std::any m_default_value; + String m_default_value_repr; + Option m_default_value_str; // used for checking default_value against choices + + std::any m_implicit_value; + + Option> m_choices { std::nullopt }; + + using valued_action = std::function; + using void_action = std::function; + + Vec> m_actions; + + std::variant m_default_action { + std::in_place_type, + [](const String& value) { return value; } + }; + + Vec 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 + StringView m_prefix_chars; // ArgumentParser has the prefix_chars + int m_usage_newline_counter = 0; + usize m_group_idx = 0; + }; + + class ArgumentParser { + public: + explicit ArgumentParser(String program_name = {}, String version = "1.0", const default_arguments add_args = default_arguments::all, const 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 String& /*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 String& /*unused*/) { + os << m_version << '\n'; + + 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 StringViews, + // references, iterators, etc. + // Many of these elements become invalidated after a copy or move. + ArgumentParser(const ArgumentParser& other) = delete; + fn operator=(const ArgumentParser& other)->ArgumentParser& = delete; + ArgumentParser(ArgumentParser&&) noexcept = delete; + fn operator=(ArgumentParser&&)->ArgumentParser& = delete; + + explicit operator bool() const { + const bool arg_used = std::ranges::any_of(m_argument_map, [](auto& it) { return it.second->m_is_used; }); + + const bool subparser_used = + std::ranges::any_of(m_subparser_used, [](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 + fn add_argument(Targs... f_args) -> Argument& { + 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; + + ~MutuallyExclusiveGroup() = default; + + fn operator=(MutuallyExclusiveGroup&&)->MutuallyExclusiveGroup& = delete; + explicit MutuallyExclusiveGroup(ArgumentParser& parent, const bool required = false) + : m_parent(parent), m_required(required), m_elements({}) {} + + MutuallyExclusiveGroup(const MutuallyExclusiveGroup& other) = delete; + + fn operator=(const MutuallyExclusiveGroup& other)->MutuallyExclusiveGroup& = 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 + fn add_argument(Targs... f_args) -> Argument& { + Argument& 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; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + bool m_required = false; + Vec m_elements; + }; + + fn add_mutually_exclusive_group(bool required = false) -> MutuallyExclusiveGroup& { + 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 + fn add_parents(const Targs&... f_args) -> ArgumentParser& { + for (const ArgumentParser& parent_parser : { std::ref(f_args)... }) { + for (const Argument& argument : parent_parser.m_positional_arguments) { + const auto it = m_positional_arguments.insert( + std::cend(m_positional_arguments), argument + ); + + index_argument(it); + } + + for (const Argument& argument : parent_parser.m_optional_arguments) { + const 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. + fn add_usage_newline() -> ArgumentParser& { + ++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. + fn add_group(String group_name) -> ArgumentParser& { + m_group_names.emplace_back(std::move(group_name)); + return *this; + } + + fn add_description(String description) -> ArgumentParser& { + m_description = std::move(description); + return *this; + } + + fn add_epilog(String epilog) -> ArgumentParser& { + 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. + fn add_hidden_alias_for(const Argument& arg, const StringView alias) -> ArgumentParser& { + for (auto it = m_optional_arguments.begin(); + it != m_optional_arguments.end(); + ++it) + if (&(*it) == &arg) { + m_argument_map.insert_or_assign(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 + fn at(const StringView name) -> T& { + if constexpr (std::is_same_v) + return (*this)[name]; + else { + const String str_name(name); + + if (const auto subparser_it = m_subparser_map.find(str_name); subparser_it != m_subparser_map.end()) + return subparser_it->second->get(); + + throw std::logic_error("No such subparser: " + str_name); + } + } + + fn set_prefix_chars(String prefix_chars) -> ArgumentParser& { + m_prefix_chars = std::move(prefix_chars); + return *this; + } + + fn set_assign_chars(String assign_chars) -> ArgumentParser& { + 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 + */ + // NOLINTNEXTLINE(misc-no-recursion) + fn parse_args(const Vec& arguments) -> void { + parse_args_internal(arguments); + // Check if all arguments are parsed + for (const auto& argument : m_argument_map | std::views::values) + argument->validate(); + + // Check each mutually exclusive group and make sure + // there are no constraint violations + for (const MutuallyExclusiveGroup& group : m_mutually_exclusive_groups) { + bool mutex_argument_used = false; + + const Argument* mutex_argument_it { nullptr }; + for (const 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 + String argument_names {}; + usize i = 0; + + const usize size = group.m_elements.size(); + + for (const Argument* arg : group.m_elements) { + if (i + 1 == size) + // last + argument_names += String("'") + arg->get_usage_full() + String("' "); + else + argument_names += String("'") + arg->get_usage_full() + 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 + */ + fn parse_known_args(const Vec& arguments) -> Vec { + Vec unknown_arguments = parse_known_args_internal(arguments); + + for (const auto& argument : m_argument_map | std::views::values) + 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(*-avoid-c-arrays) + fn parse_args(const int argc, const char* const argv[]) -> void { + 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(*-avoid-c-arrays) + fn parse_known_args(const 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 + fn get(const StringView arg_name) const -> T { + 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 + fn present(const StringView arg_name) const -> Option { + return (*this)[arg_name].present(); + } + + /* Getter that returns true for user-supplied options. Returns false if not + * user-supplied, even with a default value. + */ + [[nodiscard]] fn is_used(const StringView arg_name) const -> bool { + return (*this)[arg_name].m_is_used; + } + + /* Getter that returns true if a subcommand is used. + */ + [[nodiscard]] fn is_subcommand_used(const StringView subcommand_name) const -> bool { + return m_subparser_used.at(String(subcommand_name)); + } + + /* Getter that returns true if a subcommand is used. + */ + [[nodiscard]] fn is_subcommand_used(const ArgumentParser& subparser) const -> bool { + 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 + */ + fn operator[](const StringView arg_name) const->Argument& { + 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 char legal_prefix_char = get_any_valid_prefix_char(); + + const String prefix = 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: " + String(arg_name)); + } + + // Print help message + friend fn operator<<(std::ostream& stream, const ArgumentParser& parser)->std::ostream& { + stream.setf(std::ios_base::left); + + const usize 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::ranges::find_if(parser.m_positional_arguments, [](const Argument& argument) { return !argument.m_is_hidden; }) != + parser.m_positional_arguments.end(); + if (has_visible_positional_args) + stream << "Positional arguments:\n"; + + for (const Argument& 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 Argument& 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 (usize 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 Argument& 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; + } + } + + if (std::ranges::any_of(parser.m_subparser_map, [](auto& p) { return !p.second->get().m_suppress; })) { + 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 + [[nodiscard]] fn help() const -> std::stringstream { + std::stringstream out; + out << *this; + return out; + } + + // Sets the maximum width for a line of the Usage message + fn set_usage_max_line_width(const usize w) -> ArgumentParser& { + this->m_usage_max_line_width = w; + return *this; + } + + // Asks to display arguments of mutually exclusive group on separate lines in + // the Usage message + fn set_usage_break_on_mutex() -> ArgumentParser& { + this->m_usage_break_on_mutex = true; + return *this; + } + + // Format usage part of help only + [[nodiscard]] fn usage() const -> String { + std::stringstream stream; + + String curline("Usage: "); + curline += this->m_parser_path; + const bool multiline_usage = + this->m_usage_max_line_width < (std::numeric_limits::max)(); + const usize indent_size = curline.size(); + + const fn deal_with_options_of_group = [&](const usize group_idx) { + bool found_options = false; + + // Add any options inline here + const MutuallyExclusiveGroup* cur_mutex = nullptr; + int usage_newline_counter = -1; + + for (const Argument& 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 << '\n'; + curline = String(indent_size, ' '); + } + } + usage_newline_counter = argument.m_usage_newline_counter; + } + } + found_options = true; + const 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 << '\n'; + curline = 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 << '\n'; + curline = 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 << '\n'; + curline = 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 << '\n'; + curline = String(indent_size, ' '); + curline += " "; + } else if (cur_mutex == nullptr) { + curline += " "; + } + curline += arg_inline_usage; + } + if (cur_mutex != nullptr) { + curline += ']'; + } + return found_options; + }; + + if (const bool found_options = deal_with_options_of_group(0); found_options && multiline_usage && + !this->m_positional_arguments.empty()) { + stream << curline << '\n'; + curline = String(indent_size, ' '); + } + + // Put positional arguments after the optionals + for (const Argument& argument : this->m_positional_arguments) { + if (argument.m_is_hidden) + continue; + + const 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 << '\n'; + curline = 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 (usize i = 0; i < m_group_names.size(); ++i) { + stream << curline << '\n' + << '\n'; + stream << m_group_names[i] << ":" << '\n'; + curline = String(indent_size, ' '); + deal_with_options_of_group(i + 1); + } + + stream << curline; + + // Put subcommands after positional arguments + if (!m_subparser_map.empty()) { + stream << " {"; + usize 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(); + } + + fn add_subparser(ArgumentParser& parser) -> void { + 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); + } + + fn set_suppress(const bool suppress) -> void { + m_suppress = suppress; + } + + protected: + fn get_belonging_mutex(const Argument* arg) const -> const MutuallyExclusiveGroup* { + for (const MutuallyExclusiveGroup& mutex : m_mutually_exclusive_groups) + if (std::ranges::find(mutex.m_elements, arg) != + mutex.m_elements.end()) + return &mutex; + + return nullptr; + } + + [[nodiscard]] fn is_valid_prefix_char(const char c) const -> bool { + return m_prefix_chars.contains(c); + } + + [[nodiscard]] fn get_any_valid_prefix_char() const -> char { + 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. + */ + [[nodiscard]] fn preprocess_arguments(const Vec& raw_arguments) const -> Vec { + Vec arguments {}; + for (const String& arg : raw_arguments) { + const auto argument_starts_with_prefix_chars = + [this](const String& a) -> bool { + if (!a.empty()) { + // 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' + if (is_valid_prefix_char('/')) { + if (is_valid_prefix_char(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 (is_valid_prefix_char(a[0]) && is_valid_prefix_char(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., "=" + + if (const usize assign_char_pos = arg.find_first_of(m_assign_chars); !m_argument_map.contains(arg) && + argument_starts_with_prefix_chars(arg) && + assign_char_pos != String::npos) + // Get the name of the potential option, and check it exists + if (String opt_name = arg.substr(0, assign_char_pos); m_argument_map.contains(opt_name)) { + // 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 + */ + // NOLINTNEXTLINE(misc-no-recursion) + fn parse_args_internal(const Vec& raw_arguments) -> void { + Vec 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 String& current_argument = *it; + if (Argument::is_positional(current_argument, m_prefix_chars)) { + if (positional_argument_it == std::end(m_positional_arguments)) { + // Check sub-parsers + if (const auto subparser_it = m_subparser_map.find(current_argument); subparser_it != m_subparser_map.end()) { + // build list of remaining args + const Vec unprocessed_arguments = Vec(it, end); + + // invoke subparser + m_is_parsed = true; + m_subparser_used[current_argument] = true; + + subparser_it->second->get().parse_args( + unprocessed_arguments + ); + + return; + } + + 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 '" + + 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 Argument& 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"); + } + + throw std::runtime_error("Zero positional arguments expected"); + } + + throw std::runtime_error( + "Maximum number of positional arguments " + "exceeded, failed to parse '" + + current_argument + "'" + ); + } + + const 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()) { + const auto argument = arg_map_it->second; + + it = argument->consume(std::next(it), end, arg_map_it->first); + } else if (const String& compound_arg = current_argument; + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { + ++it; + + for (usize j = 1; j < compound_arg.size(); j++) { + const String hypothetical_arg = { '-', compound_arg[j] }; + + auto arg_map_it2 = m_argument_map.find(hypothetical_arg); + + if (arg_map_it2 != m_argument_map.end()) { + const 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 + */ + // NOLINTNEXTLINE(misc-no-recursion) + fn parse_known_args_internal(const Vec& raw_arguments) -> Vec { + Vec arguments = preprocess_arguments(raw_arguments); + + Vec unknown_arguments {}; + + if (m_program_name.empty() && !arguments.empty()) + m_program_name = arguments.front(); + + const 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 String& current_argument = *it; + if (Argument::is_positional(current_argument, m_prefix_chars)) { + if (positional_argument_it == std::end(m_positional_arguments)) { + // Check sub-parsers + if (auto subparser_it = m_subparser_map.find(current_argument); subparser_it != m_subparser_map.end()) { + // build list of remaining args + const Vec unprocessed_arguments = Vec(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 + const 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()) { + const auto argument = arg_map_it->second; + + it = argument->consume(std::next(it), end, arg_map_it->first); + } else if (const String& compound_arg = current_argument; + compound_arg.size() > 1 && + is_valid_prefix_char(compound_arg[0]) && + !is_valid_prefix_char(compound_arg[1])) { + ++it; + for (usize j = 1; j < compound_arg.size(); j++) { + const String hypothetical_arg = { '-', compound_arg[j] }; + + auto arg_map_it2 = m_argument_map.find(hypothetical_arg); + if (arg_map_it2 != m_argument_map.end()) { + const 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. + [[nodiscard]] fn get_length_of_longest_argument() const -> usize { + if (m_argument_map.empty()) + return 0; + + usize max_size = 0; + + for (const auto& argument : m_argument_map | std::views::values) + max_size = + std::max(max_size, argument->get_arguments_length()); + + for (const String& command : m_subparser_map | std::views::keys) + max_size = std::max(max_size, command.size()); + + return max_size; + } + + using argument_it = std::list::iterator; + using mutex_group_it = Vec::iterator; + using argument_parser_it = + std::list>::iterator; + + fn index_argument(argument_it it) -> void { + for (const String& name : std::as_const(it->m_names)) + m_argument_map.insert_or_assign(name, it); + } + + private: + String m_program_name; + String m_version; + String m_description; + String m_epilog; + bool m_exit_on_default_arguments = true; + String m_prefix_chars { "-" }; + String m_assign_chars { "=" }; + bool m_is_parsed = false; + std::list m_positional_arguments; + std::list m_optional_arguments; + Map m_argument_map; + String m_parser_path; + std::list> m_subparsers; + Map m_subparser_map; + Map m_subparser_used; + Vec m_mutually_exclusive_groups; + bool m_suppress = false; + usize m_usage_max_line_width = (std::numeric_limits::max)(); + bool m_usage_break_on_mutex = false; + int m_usage_newline_counter = 0; + Vec m_group_names; + }; +} // namespace argparse + +// NOLINTEND(readability-identifier-naming, readability-identifier-length) diff --git a/include/matchit.h b/include/matchit.hpp similarity index 94% rename from include/matchit.h rename to include/matchit.hpp index 76dcb58..9badaf6 100644 --- a/include/matchit.h +++ b/include/matchit.hpp @@ -40,7 +40,8 @@ namespace matchit { public: template - constexpr explicit MatchHelper(V&& value) : mValue { std::forward(value) } {} + constexpr explicit MatchHelper(V&& value) + : mValue { std::forward(value) } {} template constexpr auto operator()(const PatternPair&... patterns) { return matchPatterns(std::forward(mValue), patterns...); @@ -116,14 +117,18 @@ namespace matchit { template class EvalTraits> { public: - constexpr static decltype(auto) evalImpl(const Nullary& e) { return e(); } + constexpr static decltype(auto) evalImpl(const Nullary& e) { + return e(); + } }; // Only allowed in nullary template class EvalTraits> { public: - constexpr static decltype(auto) evalImpl(const Id& id) { return *const_cast&>(id); } + constexpr static decltype(auto) evalImpl(const Id& id) { + return *const_cast&>(id); + } }; template @@ -283,9 +288,11 @@ namespace matchit { S mEnd; public: - constexpr Subrange(const I begin, const S end) : mBegin { begin }, mEnd { end } {} + constexpr Subrange(const I begin, const S end) + : mBegin { begin }, mEnd { end } {} - constexpr Subrange(const Subrange& other) : mBegin { other.begin() }, mEnd { other.end() } {} + constexpr Subrange(const Subrange& other) + : mBegin { other.begin() }, mEnd { other.end() } {} Subrange& operator=(const Subrange& other) { mBegin = other.begin(); @@ -293,9 +300,15 @@ namespace matchit { return *this; } - size_t size() const { return static_cast(std::distance(mBegin, mEnd)); } - auto begin() const { return mBegin; } - auto end() const { return mEnd; } + size_t size() const { + return static_cast(std::distance(mBegin, mEnd)); + } + auto begin() const { + return mBegin; + } + auto end() const { + return mEnd; + } }; template @@ -465,7 +478,8 @@ namespace matchit { using RetType = std::common_type_t; }; - enum class IdProcess : int32_t { kCANCEL, kCONFIRM }; + enum class IdProcess : int32_t { kCANCEL, + kCONFIRM }; template constexpr void processId(const Pattern& pattern, int32_t depth, IdProcess idProcess) { @@ -497,7 +511,9 @@ namespace matchit { mMemHolder[mSize] = std::forward(t); ++mSize; } - constexpr auto back() -> ElementT& { return mMemHolder[mSize - 1]; } + constexpr auto back() -> ElementT& { + return mMemHolder[mSize - 1]; + } }; template <> @@ -514,7 +530,7 @@ namespace matchit { template constexpr auto matchPattern(Value&& value, const Pattern& pattern, int32_t depth, ConctextT& context) { - const auto result = PatternTraits::matchPatternImpl(std::forward(value), pattern, depth, context); + const auto result = PatternTraits::matchPatternImpl(std::forward(value), pattern, depth, context); const auto process = result ? IdProcess::kCONFIRM : IdProcess::kCANCEL; processId(pattern, depth, process); return result; @@ -526,12 +542,15 @@ namespace matchit { using RetType = std::invoke_result_t; using PatternT = Pattern; - constexpr PatternPair(const Pattern& pattern, const Func& func) : mPattern { pattern }, mHandler { func } {} + constexpr PatternPair(const Pattern& pattern, const Func& func) + : mPattern { pattern }, mHandler { func } {} template constexpr bool matchValue(Value&& value, ContextT& context) const { return matchPattern(std::forward(value), mPattern, /*depth*/ 0, context); } - constexpr auto execute() const { return mHandler(); } + constexpr auto execute() const { + return mHandler(); + } private: const Pattern mPattern; @@ -556,7 +575,8 @@ namespace matchit { template class PatternHelper { public: - constexpr explicit PatternHelper(const Pattern& pattern) : mPattern { pattern } {} + constexpr explicit PatternHelper(const Pattern& pattern) + : mPattern { pattern } {} template constexpr auto operator=(Func&& func) { auto f = toNullary(func); @@ -641,8 +661,11 @@ namespace matchit { template class Or { public: - constexpr explicit Or(const Patterns&... patterns) : mPatterns { patterns... } {} - constexpr const auto& patterns() const { return mPatterns; } + constexpr explicit Or(const Patterns&... patterns) + : mPatterns { patterns... } {} + constexpr const auto& patterns() const { + return mPatterns; + } private: std::tuple...> mPatterns; @@ -713,8 +736,12 @@ namespace matchit { public: constexpr App(Unary&& unary, const Pattern& pattern) : mUnary { std::forward(unary) }, mPattern { pattern } {} - constexpr const auto& unary() const { return mUnary; } - constexpr const auto& pattern() const { return mPattern; } + constexpr const auto& unary() const { + return mUnary; + } + constexpr const auto& pattern() const { + return mPattern; + } private: const Unary mUnary; @@ -775,8 +802,11 @@ namespace matchit { template class And { public: - constexpr explicit And(const Patterns&... patterns) : mPatterns { patterns... } {} - constexpr const auto& patterns() const { return mPatterns; } + constexpr explicit And(const Patterns&... patterns) + : mPatterns { patterns... } {} + constexpr const auto& patterns() const { + return mPatterns; + } private: std::tuple...> mPatterns; @@ -835,8 +865,11 @@ namespace matchit { template class Not { public: - explicit Not(const Pattern& pattern) : mPattern { pattern } {} - const auto& pattern() const { return mPattern; } + explicit Not(const Pattern& pattern) + : mPattern { pattern } {} + const auto& pattern() const { + return mPattern; + } private: InternalPatternT mPattern; @@ -935,10 +968,13 @@ namespace matchit { ValueVariant mVariant; public: - constexpr IdBlockBase() : mDepth {}, mVariant {} {} + constexpr IdBlockBase() + : mDepth {}, mVariant {} {} - constexpr auto& variant() { return mVariant; } - constexpr void reset(const int32_t depth) { + constexpr auto& variant() { + return mVariant; + } + constexpr void reset(const int32_t depth) { if (mDepth - depth >= 0) { mVariant = {}; mDepth = depth; @@ -1061,12 +1097,16 @@ namespace matchit { using BlockVT = std::variant; BlockVT mBlock = BlockT {}; - constexpr decltype(auto) internalValue() const { return block().get(); } + constexpr decltype(auto) internalValue() const { + return block().get(); + } public: constexpr Id() = default; - constexpr Id(const Id& id) { mBlock = BlockVT { &id.block() }; } + constexpr Id(const Id& id) { + mBlock = BlockVT { &id.block() }; + } // non-const to inform users not to mark Id as const. template @@ -1075,7 +1115,9 @@ namespace matchit { } // non-const to inform users not to mark Id as const. - constexpr auto at(const Ooo&) { return OooBinder { *this }; } + constexpr auto at(const Ooo&) { + return OooBinder { *this }; + } constexpr BlockT& block() const { return std::visit( @@ -1094,13 +1136,23 @@ namespace matchit { IdUtil::bindValue(block().variant(), std::forward(v), StorePointer {}); return true; } - constexpr void reset(int32_t depth) const { return block().reset(depth); } - constexpr void confirm(int32_t depth) const { return block().confirm(depth); } - constexpr bool hasValue() const { return block().hasValue(); } + constexpr void reset(int32_t depth) const { + return block().reset(depth); + } + constexpr void confirm(int32_t depth) const { + return block().confirm(depth); + } + constexpr bool hasValue() const { + return block().hasValue(); + } // non-const to inform users not to mark Id as const. - constexpr decltype(auto) get() { return block().get(); } + constexpr decltype(auto) get() { + return block().get(); + } // non-const to inform users not to mark Id as const. - constexpr decltype(auto) operator*() { return get(); } + constexpr decltype(auto) operator*() { + return get(); + } }; template @@ -1127,8 +1179,11 @@ namespace matchit { template class Ds { public: - constexpr explicit Ds(const Patterns&... patterns) : mPatterns { patterns... } {} - constexpr const auto& patterns() const { return mPatterns; } + constexpr explicit Ds(const Patterns&... patterns) + : mPatterns { patterns... } {} + constexpr const auto& patterns() const { + return mPatterns; + } using Type = std::tuple...>; @@ -1146,8 +1201,11 @@ namespace matchit { Id mId; public: - explicit OooBinder(const Id& id) : mId { id } {} - decltype(auto) binder() const { return mId; } + explicit OooBinder(const Id& id) + : mId { id } {} + decltype(auto) binder() const { + return mId; + } }; class Ooo { @@ -1404,7 +1462,7 @@ namespace matchit { using Vs0 = SubTypesT<0, idxOoo, std::tuple>; constexpr static auto isBinder = isOooBinderV>>; // <0, ...int32_t> to workaround compile failure for std::tuple<>. - using ElemT = std::tuple_element_t<0, std::tuple..., int32_t>>; + using ElemT = std::tuple_element_t<0, std::tuple..., int32_t>>; constexpr static int64_t diff = static_cast(sizeof...(Values) - sizeof...(Patterns)); constexpr static size_t clippedDiff = static_cast(diff > 0 ? diff : 0); using OooResultTuple = @@ -1522,8 +1580,8 @@ namespace matchit { } constexpr auto idxOoo = findOooIdx::Type>(); constexpr auto isBinder = isOooBinderV>>; - auto result = matchPatternRange<0, idxOoo>(std::begin(valueRange), dsPat.patterns(), depth, context); - const auto valLen = valueRange.size(); + auto result = matchPatternRange<0, idxOoo>(std::begin(valueRange), dsPat.patterns(), depth, context); + const auto valLen = valueRange.size(); constexpr auto patLen = sizeof...(Patterns); const auto beginOoo = std::next(std::begin(valueRange), idxOoo); if constexpr (isBinder) { @@ -1568,9 +1626,14 @@ namespace matchit { template class PostCheck { public: - constexpr explicit PostCheck(const Pattern& pattern, const Pred& pred) : mPattern { pattern }, mPred { pred } {} - constexpr bool check() const { return mPred(); } - constexpr const auto& pattern() const { return mPattern; } + constexpr explicit PostCheck(const Pattern& pattern, const Pred& pred) + : mPattern { pattern }, mPred { pred } {} + constexpr bool check() const { + return mPred(); + } + constexpr const auto& pattern() const { + return mPattern; + } private: const Pattern mPattern; @@ -1742,9 +1805,13 @@ namespace matchit { return dynamic_cast(std::addressof(b)); } - constexpr auto operator()(const T& b) const { return std::addressof(b); } + constexpr auto operator()(const T& b) const { + return std::addressof(b); + } - constexpr auto operator()(T& b) const { return std::addressof(b); } + constexpr auto operator()(T& b) const { + return std::addressof(b); + } }; static_assert(std::is_invocable_v, int>); diff --git a/meson.build b/meson.build index 7a0a4dc..a7ab820 100644 --- a/meson.build +++ b/meson.build @@ -88,7 +88,14 @@ add_project_arguments(common_cpp_args, language : 'cpp') # ------- # # Files # # ------- # -base_sources = files('src/core/system_data.cpp', 'src/core/package.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp') +base_sources = files( + 'src/core/system_data.cpp', + 'src/core/package.cpp', + 'src/config/config.cpp', + 'src/config/weather.cpp', + 'src/ui/ui.cpp', + 'src/main.cpp', +) platform_sources = { 'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], diff --git a/src/config/config.cpp b/src/config/config.cpp index c60bbe5..0b38c43 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -124,7 +124,9 @@ location = "London" # Your city name const Result envUser = util::helpers::GetEnv("USER"); const Result envLogname = util::helpers::GetEnv("LOGNAME"); - defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User"; + defaultName = pwdName ? pwdName : envUser ? *envUser + : envLogname ? *envLogname + : "User"; #endif std::ofstream file(configPath); diff --git a/src/config/config.hpp b/src/config/config.hpp index 16338e3..3a2ba66 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -10,12 +10,11 @@ #else #include // getpwuid, passwd #include // getuid - - #include "src/util/helpers.hpp" #endif #include "src/util/defs.hpp" #include "src/util/error.hpp" +#include "src/util/helpers.hpp" #include "src/util/logging.hpp" #include "src/util/types.hpp" @@ -92,7 +91,9 @@ struct NowPlaying { * @param tbl The TOML table to parse, containing [now_playing]. * @return A NowPlaying instance with the parsed values, or defaults otherwise. */ - static fn fromToml(const toml::table& tbl) -> NowPlaying { return { .enabled = tbl["enabled"].value_or(false) }; } + static fn fromToml(const toml::table& tbl) -> NowPlaying { + return { .enabled = tbl["enabled"].value_or(false) }; + } }; /** diff --git a/src/config/weather.cpp b/src/config/weather.cpp index ab94f47..9ac438e 100644 --- a/src/config/weather.cpp +++ b/src/config/weather.cpp @@ -10,8 +10,8 @@ #include // glz::{error_ctx, error_code} #include // glz::opts #include // glz::format_error -#include // NOLINT(misc-include-cleaner) - glaze/json/read.hpp is needed for glz::read -#include // std::{get, holds_alternative} +#include // NOLINT(misc-include-cleaner) - glaze/json/read.hpp is needed for glz::read +#include // std::{get, holds_alternative} #include "src/util/cache.hpp" #include "src/util/defs.hpp" diff --git a/src/main.cpp b/src/main.cpp index 2dc9da1..71b2cf0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,9 @@ #include // std::format #include // ftxui::{Element, hbox, vbox, text, separator, filler, etc.} #include // ftxui::{Render} -#include // ftxui::Color #include // ftxui::{Screen, Dimension::Full} -#include // ftxui::string_width -#include // std::ranges::{iota, to, transform} + +#include "src/ui/ui.hpp" #ifdef __cpp_lib_print #include // std::print @@ -13,341 +12,57 @@ #endif #include "src/config/config.hpp" -#include "src/config/weather.hpp" #include "src/core/system_data.hpp" #include "src/util/defs.hpp" #include "src/util/logging.hpp" #include "src/util/types.hpp" -namespace ui { - using ftxui::Color; - using util::types::StringView, util::types::i32; +#include "include/argparse.hpp" - // Color themes - struct Theme { - Color::Palette16 icon; - Color::Palette16 label; - Color::Palette16 value; - Color::Palette16 border; - }; +using util::types::i32; - static constexpr Theme DEFAULT_THEME = { - .icon = Color::Cyan, - .label = Color::Yellow, - .value = Color::White, - .border = Color::GrayLight, - }; - - struct Icons { - StringView user; - StringView palette; - StringView calendar; - StringView host; - StringView kernel; - StringView os; - StringView memory; - StringView weather; - StringView music; - StringView disk; - StringView shell; - StringView package; - StringView desktop; - StringView windowManager; - }; - - [[maybe_unused]] static constexpr Icons NONE = { - .user = "", - .palette = "", - .calendar = "", - .host = "", - .kernel = "", - .os = "", - .memory = "", - .weather = "", - .music = "", - .disk = "", - .shell = "", - .package = "", - .desktop = "", - .windowManager = "", - }; - - [[maybe_unused]] static constexpr Icons NERD = { - .user = "  ", - .palette = "  ", - .calendar = "  ", - .host = " 󰌢 ", - .kernel = "  ", - .os = "  ", - .memory = "  ", - .weather = "  ", - .music = "  ", - .disk = " 󰋊 ", - .shell = "  ", - .package = " 󰏖 ", - .desktop = " 󰇄 ", - .windowManager = "  ", - }; - - [[maybe_unused]] static constexpr Icons EMOJI = { - .user = " 👤 ", - .palette = " 🎨 ", - .calendar = " 📅 ", - .host = " 💻 ", - .kernel = " 🫀 ", - .os = " 🤖 ", - .memory = " 🧠 ", - .weather = " 🌈 ", - .music = " 🎵 ", - .disk = " 💾 ", - .shell = " 💲 ", - .package = " 📦 ", - .desktop = " 🖥️ ", - .windowManager = " 🪟 ", - }; - - static constexpr inline Icons ICON_TYPE = NERD; -} // namespace ui - -namespace { - using namespace util::logging; +fn main(const i32 argc, char* argv[]) -> i32 { using namespace ftxui; - - struct RowInfo { - StringView icon; - StringView label; - String value; - }; - - fn CreateColorCircles() -> Element { - auto colorView = - std::views::iota(0, 16) | std::views::transform([](i32 colorIndex) { - return ftxui::hbox( - { ftxui::text("◯") | ftxui::bold | ftxui::color(static_cast(colorIndex)), - ftxui::text(" ") } - ); - }); - - return hbox(Elements(std::ranges::begin(colorView), std::ranges::end(colorView))); - } - - fn get_visual_width(const String& str) -> usize { return ftxui::string_width(str); } - fn get_visual_width_sv(const StringView& sview) -> usize { return ftxui::string_width(String(sview)); } - - fn find_max_label_len(const std::vector& rows) -> usize { - usize maxWidth = 0; - for (const RowInfo& row : rows) maxWidth = std::max(maxWidth, get_visual_width_sv(row.label)); - - return maxWidth; - }; - - fn SystemInfoBox(const Config& config, const os::SystemData& data) -> Element { - const String& name = config.general.name; - const Weather& weather = config.weather; - - const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, packageIcon, deIcon, wmIcon] = - ui::ICON_TYPE; - - std::vector initialRows; // Date, Weather - std::vector systemInfoRows; // Host, Kernel, OS, RAM, Disk, Shell, Packages - std::vector envInfoRows; // DE, WM - - if (data.date) - initialRows.push_back({ .icon = calendarIcon, .label = "Date", .value = *data.date }); - - if (weather.enabled && data.weather) { - const weather::Output& weatherInfo = *data.weather; - String weatherValue = weather.showTownName - ? std::format("{}°F in {}", std::lround(weatherInfo.main.temp), weatherInfo.name) - : std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description); - initialRows.push_back({ .icon = weatherIcon, .label = "Weather", .value = std::move(weatherValue) }); - } else if (weather.enabled && !data.weather.has_value()) - debug_at(data.weather.error()); - - if (data.host && !data.host->empty()) - systemInfoRows.push_back({ .icon = hostIcon, .label = "Host", .value = *data.host }); - - if (data.osVersion) - systemInfoRows.push_back({ .icon = osIcon, .label = "OS", .value = *data.osVersion }); - - if (data.kernelVersion) - systemInfoRows.push_back({ .icon = kernelIcon, .label = "Kernel", .value = *data.kernelVersion }); - - if (data.memInfo) - systemInfoRows.push_back( - { .icon = memoryIcon, .label = "RAM", .value = std::format("{}", BytesToGiB { *data.memInfo }) } - ); - else if (!data.memInfo.has_value()) - debug_at(data.memInfo.error()); - - if (data.diskUsage) - systemInfoRows.push_back( - { - .icon = diskIcon, - .label = "Disk", - .value = - std::format("{}/{}", BytesToGiB { data.diskUsage->usedBytes }, BytesToGiB { data.diskUsage->totalBytes }), - } - ); - - if (data.shell) - systemInfoRows.push_back({ .icon = shellIcon, .label = "Shell", .value = *data.shell }); - - if (data.packageCount) { - if (*data.packageCount > 0) - systemInfoRows.push_back( - { .icon = packageIcon, .label = "Packages", .value = std::format("{}", *data.packageCount) } - ); - else - debug_log("Package count is 0, skipping"); - } - - bool addedDe = false; - if (data.desktopEnv && (!data.windowMgr || *data.desktopEnv != *data.windowMgr)) { - envInfoRows.push_back({ .icon = deIcon, .label = "DE", .value = *data.desktopEnv }); - addedDe = true; - } - - if (data.windowMgr) { - if (!addedDe || (data.desktopEnv && *data.desktopEnv != *data.windowMgr)) - envInfoRows.push_back({ .icon = wmIcon, .label = "WM", .value = *data.windowMgr }); - } - - bool nowPlayingActive = false; - String npText; - - if (config.nowPlaying.enabled && data.nowPlaying) { - const String title = data.nowPlaying->title.value_or("Unknown Title"); - const String artist = data.nowPlaying->artist.value_or("Unknown Artist"); - npText = artist + " - " + title; - nowPlayingActive = true; - } - - usize maxContentWidth = 0; - - const usize greetingWidth = get_visual_width_sv(userIcon) + get_visual_width_sv("Hello ") + get_visual_width(name) + - get_visual_width_sv("! "); - maxContentWidth = std::max(maxContentWidth, greetingWidth); - - const usize paletteWidth = - get_visual_width_sv(userIcon) + (16 * (get_visual_width_sv("◯") + get_visual_width_sv(" "))); - maxContentWidth = std::max(maxContentWidth, paletteWidth); - - const usize iconActualWidth = get_visual_width_sv(userIcon); - - const usize maxLabelWidthInitial = find_max_label_len(initialRows); - const usize maxLabelWidthSystem = find_max_label_len(systemInfoRows); - const usize maxLabelWidthEnv = find_max_label_len(envInfoRows); - - const usize requiredWidthInitialW = iconActualWidth + maxLabelWidthInitial; - const usize requiredWidthSystemW = iconActualWidth + maxLabelWidthSystem; - const usize requiredWidthEnvW = iconActualWidth + maxLabelWidthEnv; - - fn calculateRowVisualWidth = [&](const RowInfo& row, const usize requiredLabelVisualWidth) -> usize { - return requiredLabelVisualWidth + get_visual_width(row.value) + get_visual_width_sv(" "); - }; - - for (const RowInfo& row : initialRows) - maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthInitialW)); - - for (const RowInfo& row : systemInfoRows) - maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthSystemW)); - - for (const RowInfo& row : envInfoRows) - maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthEnvW)); - - const usize targetBoxWidth = maxContentWidth + 2; - - usize npFixedWidthLeft = 0; - usize npFixedWidthRight = 0; - - if (nowPlayingActive) { - npFixedWidthLeft = get_visual_width_sv(musicIcon) + get_visual_width_sv("Playing") + get_visual_width_sv(" "); - npFixedWidthRight = get_visual_width_sv(" "); - } - - i32 paragraphLimit = 1; - - if (nowPlayingActive) { - i32 availableForParagraph = - static_cast(targetBoxWidth) - static_cast(npFixedWidthLeft) - static_cast(npFixedWidthRight); - - availableForParagraph -= 2; - - paragraphLimit = std::max(1, availableForParagraph); - } - - fn createStandardRow = [&](const RowInfo& row, const usize sectionRequiredVisualWidth) { - return hbox( - { - hbox( - { - text(String(row.icon)) | color(ui::DEFAULT_THEME.icon), - text(String(row.label)) | color(ui::DEFAULT_THEME.label), - } - ) | - size(WIDTH, EQUAL, static_cast(sectionRequiredVisualWidth)), - filler(), - text(row.value) | color(ui::DEFAULT_THEME.value), - text(" "), - } - ); - }; - - Elements content; - - content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan)); - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - content.push_back(hbox({ text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), CreateColorCircles() })); - - const bool section1Present = !initialRows.empty(); - const bool section2Present = !systemInfoRows.empty(); - const bool section3Present = !envInfoRows.empty(); - - if (section1Present) - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - - for (const RowInfo& row : initialRows) content.push_back(createStandardRow(row, requiredWidthInitialW)); - - if ((section1Present && (section2Present || section3Present)) || (!section1Present && section2Present)) - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - - for (const RowInfo& row : systemInfoRows) content.push_back(createStandardRow(row, requiredWidthSystemW)); - - if (section2Present && section3Present) - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - - for (const RowInfo& row : envInfoRows) content.push_back(createStandardRow(row, requiredWidthEnvW)); - - if ((section1Present || section2Present || section3Present) && nowPlayingActive) - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - - if (nowPlayingActive) { - content.push_back(hbox( - { text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), - text("Playing") | color(ui::DEFAULT_THEME.label), - text(" "), - filler(), - paragraphAlignRight(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, paragraphLimit), - text(" ") } - )); - } - - return vbox(content) | borderRounded | color(Color::White); - } -} // namespace - -fn main() -> i32 { using os::SystemData; #ifdef _WIN32 winrt::init_apartment(); #endif + argparse::ArgumentParser parser("draconis", "0.1.0"); + + parser + .add_argument("--log-level") + .help("Set the log level") + .default_value("info") + .choices("trace", "debug", "info", "warn", "error", "fatal"); + + parser + .add_argument("-V", "--verbose") + .help("Enable verbose logging. Alias for --log-level=debug") + .flag(); + + try { + parser.parse_args(argc, argv); + } catch (const util::types::Exception& err) { +#ifdef __cpp_lib_print + std::println(stderr, "{}", err.what()); +#else + std::cerr << err.what() << '\n'; +#endif + + std::cerr << parser; + + return 1; + } + + if (parser["--verbose"] == true || parser["-v"] == true) + info_log("Verbose logging enabled"); + const Config& config = Config::getInstance(); const SystemData data = SystemData(config); - Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }) }); + Element document = ui::CreateUI(config, data); Screen screen = Screen::Create(Dimension::Full(), Dimension::Fit(document)); Render(screen, document); diff --git a/src/os/haiku.cpp b/src/os/haiku.cpp index 7feddb0..ebc2e26 100644 --- a/src/os/haiku.cpp +++ b/src/os/haiku.cpp @@ -72,9 +72,13 @@ namespace os { return Err(DracError(DracErrorCode::NotSupported, "Now playing is not supported on Haiku")); } - fn GetWindowManager() -> Result { return "app_server"; } + fn GetWindowManager() -> Result { + return "app_server"; + } - fn GetDesktopEnvironment() -> Result { return "Haiku Desktop Environment"; } + fn GetDesktopEnvironment() -> Result { + return "Haiku Desktop Environment"; + } fn GetShell() -> Result { if (const Result shellPath = GetEnv("SHELL")) { diff --git a/src/os/macos.cpp b/src/os/macos.cpp index 282e4ff..a8d7181 100644 --- a/src/os/macos.cpp +++ b/src/os/macos.cpp @@ -48,15 +48,26 @@ namespace os { return mem; } - fn GetNowPlaying() -> Result { return GetCurrentPlayingInfo(); } + fn GetNowPlaying() -> Result { + return GetCurrentPlayingInfo(); + } - fn GetOSVersion() -> Result { return GetMacOSVersion(); } + fn GetOSVersion() -> Result { + return GetMacOSVersion(); + } - fn GetDesktopEnvironment() -> Result { return "Aqua"; } + fn GetDesktopEnvironment() -> Result { + return "Aqua"; + } fn GetWindowManager() -> Result { constexpr Array knownWms = { - "yabai", "kwm", "chunkwm", "amethyst", "spectacle", "rectangle", + "yabai", + "kwm", + "chunkwm", + "amethyst", + "spectacle", + "rectangle", }; Array request = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; diff --git a/src/os/serenity.cpp b/src/os/serenity.cpp index 6dbb962..bfdd194 100644 --- a/src/os/serenity.cpp +++ b/src/os/serenity.cpp @@ -110,9 +110,13 @@ namespace os { return Err(DracError(DracErrorCode::NotSupported, "Now playing is not supported on SerenityOS")); } - fn GetWindowManager() -> Result { return "WindowManager"; } + fn GetWindowManager() -> Result { + return "WindowManager"; + } - fn GetDesktopEnvironment() -> Result { return "SerenityOS Desktop"; } + fn GetDesktopEnvironment() -> Result { + return "SerenityOS Desktop"; + } fn GetShell() -> Result { uid_t userId = getuid(); @@ -166,7 +170,9 @@ namespace os { } // namespace os namespace package { - fn GetSerenityCount() -> Result { return CountUniquePackages("/usr/Ports/installed.db"); } + fn GetSerenityCount() -> Result { + return CountUniquePackages("/usr/Ports/installed.db"); + } } // namespace package #endif // __serenity__ diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp new file mode 100644 index 0000000..e137dc7 --- /dev/null +++ b/src/ui/ui.cpp @@ -0,0 +1,306 @@ +#include "ui.hpp" + +#include "src/util/types.hpp" + +namespace ui { + using namespace ftxui; + using namespace util::types; + + constexpr Theme DEFAULT_THEME = { + .icon = Color::Cyan, + .label = Color::Yellow, + .value = Color::White, + .border = Color::GrayLight, + }; + + [[maybe_unused]] static constexpr Icons NONE = { + .user = "", + .palette = "", + .calendar = "", + .host = "", + .kernel = "", + .os = "", + .memory = "", + .weather = "", + .music = "", + .disk = "", + .shell = "", + .package = "", + .desktop = "", + .windowManager = "", + }; + + [[maybe_unused]] static constexpr Icons NERD = { + .user = "  ", + .palette = "  ", + .calendar = "  ", + .host = " 󰌢 ", + .kernel = "  ", + .os = "  ", + .memory = "  ", + .weather = "  ", + .music = "  ", + .disk = " 󰋊 ", + .shell = "  ", + .package = " 󰏖 ", + .desktop = " 󰇄 ", + .windowManager = "  ", + }; + + [[maybe_unused]] static constexpr Icons EMOJI = { + .user = " 👤 ", + .palette = " 🎨 ", + .calendar = " 📅 ", + .host = " 💻 ", + .kernel = " 🫀 ", + .os = " 🤖 ", + .memory = " 🧠 ", + .weather = " 🌈 ", + .music = " 🎵 ", + .disk = " 💾 ", + .shell = " 💲 ", + .package = " 📦 ", + .desktop = " 🖥️ ", + .windowManager = " 🪟 ", + }; + + constexpr inline Icons ICON_TYPE = NERD; + + struct RowInfo { + StringView icon; + StringView label; + String value; + }; + + namespace { + fn CreateColorCircles() -> Element { + auto colorView = + std::views::iota(0, 16) | std::views::transform([](i32 colorIndex) { + return ftxui::hbox( + { ftxui::text("◯") | ftxui::bold | ftxui::color(static_cast(colorIndex)), + ftxui::text(" ") } + ); + }); + + return hbox(Elements(std::ranges::begin(colorView), std::ranges::end(colorView))); + } + + fn get_visual_width(const String& str) -> usize { + return ftxui::string_width(str); + } + + fn get_visual_width_sv(const StringView& sview) -> usize { + return ftxui::string_width(String(sview)); + } + + fn find_max_label_len(const std::vector& rows) -> usize { + usize maxWidth = 0; + for (const RowInfo& row : rows) maxWidth = std::max(maxWidth, get_visual_width_sv(row.label)); + + return maxWidth; + }; + + fn CreateInfoBox(const Config& config, const os::SystemData& data) -> Element { + const String& name = config.general.name; + const Weather& weather = config.weather; + + const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, packageIcon, deIcon, wmIcon] = + ui::ICON_TYPE; + + std::vector initialRows; // Date, Weather + std::vector systemInfoRows; // Host, Kernel, OS, RAM, Disk, Shell, Packages + std::vector envInfoRows; // DE, WM + + if (data.date) + initialRows.push_back({ .icon = calendarIcon, .label = "Date", .value = *data.date }); + + if (weather.enabled && data.weather) { + const weather::Output& weatherInfo = *data.weather; + String weatherValue = weather.showTownName + ? std::format("{}°F in {}", std::lround(weatherInfo.main.temp), weatherInfo.name) + : std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description); + initialRows.push_back({ .icon = weatherIcon, .label = "Weather", .value = std::move(weatherValue) }); + } else if (weather.enabled && !data.weather.has_value()) + debug_at(data.weather.error()); + + if (data.host && !data.host->empty()) + systemInfoRows.push_back({ .icon = hostIcon, .label = "Host", .value = *data.host }); + + if (data.osVersion) + systemInfoRows.push_back({ .icon = osIcon, .label = "OS", .value = *data.osVersion }); + + if (data.kernelVersion) + systemInfoRows.push_back({ .icon = kernelIcon, .label = "Kernel", .value = *data.kernelVersion }); + + if (data.memInfo) + systemInfoRows.push_back( + { .icon = memoryIcon, .label = "RAM", .value = std::format("{}", BytesToGiB { *data.memInfo }) } + ); + else if (!data.memInfo.has_value()) + debug_at(data.memInfo.error()); + + if (data.diskUsage) + systemInfoRows.push_back( + { + .icon = diskIcon, + .label = "Disk", + .value = + std::format("{}/{}", BytesToGiB { data.diskUsage->usedBytes }, BytesToGiB { data.diskUsage->totalBytes }), + } + ); + + if (data.shell) + systemInfoRows.push_back({ .icon = shellIcon, .label = "Shell", .value = *data.shell }); + + if (data.packageCount) { + if (*data.packageCount > 0) + systemInfoRows.push_back( + { .icon = packageIcon, .label = "Packages", .value = std::format("{}", *data.packageCount) } + ); + else + debug_log("Package count is 0, skipping"); + } + + bool addedDe = false; + + if (data.desktopEnv && (!data.windowMgr || *data.desktopEnv != *data.windowMgr)) { + envInfoRows.push_back({ .icon = deIcon, .label = "DE", .value = *data.desktopEnv }); + addedDe = true; + } + + if (data.windowMgr) + if (!addedDe || (data.desktopEnv && *data.desktopEnv != *data.windowMgr)) + envInfoRows.push_back({ .icon = wmIcon, .label = "WM", .value = *data.windowMgr }); + + bool nowPlayingActive = false; + String npText; + + if (config.nowPlaying.enabled && data.nowPlaying) { + const String title = data.nowPlaying->title.value_or("Unknown Title"); + const String artist = data.nowPlaying->artist.value_or("Unknown Artist"); + npText = artist + " - " + title; + nowPlayingActive = true; + } + + usize maxContentWidth = 0; + + const usize greetingWidth = get_visual_width_sv(userIcon) + get_visual_width_sv("Hello ") + get_visual_width(name) + + get_visual_width_sv("! "); + maxContentWidth = std::max(maxContentWidth, greetingWidth); + + const usize paletteWidth = + get_visual_width_sv(userIcon) + (16 * (get_visual_width_sv("◯") + get_visual_width_sv(" "))); + maxContentWidth = std::max(maxContentWidth, paletteWidth); + + const usize iconActualWidth = get_visual_width_sv(userIcon); + + const usize maxLabelWidthInitial = find_max_label_len(initialRows); + const usize maxLabelWidthSystem = find_max_label_len(systemInfoRows); + const usize maxLabelWidthEnv = find_max_label_len(envInfoRows); + + const usize requiredWidthInitialW = iconActualWidth + maxLabelWidthInitial; + const usize requiredWidthSystemW = iconActualWidth + maxLabelWidthSystem; + const usize requiredWidthEnvW = iconActualWidth + maxLabelWidthEnv; + + fn calculateRowVisualWidth = [&](const RowInfo& row, const usize requiredLabelVisualWidth) -> usize { + return requiredLabelVisualWidth + get_visual_width(row.value) + get_visual_width_sv(" "); + }; + + for (const RowInfo& row : initialRows) + maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthInitialW)); + + for (const RowInfo& row : systemInfoRows) + maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthSystemW)); + + for (const RowInfo& row : envInfoRows) + maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthEnvW)); + + const usize targetBoxWidth = maxContentWidth + 2; + + usize npFixedWidthLeft = 0; + usize npFixedWidthRight = 0; + + if (nowPlayingActive) { + npFixedWidthLeft = get_visual_width_sv(musicIcon) + get_visual_width_sv("Playing") + get_visual_width_sv(" "); + npFixedWidthRight = get_visual_width_sv(" "); + } + + i32 paragraphLimit = 1; + + if (nowPlayingActive) { + i32 availableForParagraph = + static_cast(targetBoxWidth) - static_cast(npFixedWidthLeft) - static_cast(npFixedWidthRight); + + availableForParagraph -= 2; + + paragraphLimit = std::max(1, availableForParagraph); + } + + fn createStandardRow = [&](const RowInfo& row, const usize sectionRequiredVisualWidth) { + return hbox( + { + hbox( + { + text(String(row.icon)) | color(ui::DEFAULT_THEME.icon), + text(String(row.label)) | color(ui::DEFAULT_THEME.label), + } + ) | + size(WIDTH, EQUAL, static_cast(sectionRequiredVisualWidth)), + filler(), + text(row.value) | color(ui::DEFAULT_THEME.value), + text(" "), + } + ); + }; + + Elements content; + + content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan)); + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + content.push_back(hbox({ text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), CreateColorCircles() })); + + const bool section1Present = !initialRows.empty(); + const bool section2Present = !systemInfoRows.empty(); + const bool section3Present = !envInfoRows.empty(); + + if (section1Present) + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + + for (const RowInfo& row : initialRows) content.push_back(createStandardRow(row, requiredWidthInitialW)); + + if ((section1Present && (section2Present || section3Present)) || (!section1Present && section2Present)) + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + + for (const RowInfo& row : systemInfoRows) content.push_back(createStandardRow(row, requiredWidthSystemW)); + + if (section2Present && section3Present) + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + + for (const RowInfo& row : envInfoRows) content.push_back(createStandardRow(row, requiredWidthEnvW)); + + if ((section1Present || section2Present || section3Present) && nowPlayingActive) + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + + if (nowPlayingActive) { + content.push_back(hbox( + { + text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), + text("Playing") | color(ui::DEFAULT_THEME.label), + text(" "), + filler(), + paragraphAlignRight(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, paragraphLimit), + text(" "), + } + )); + } + + return vbox(content) | borderRounded | color(Color::White); + } + } // namespace + + fn CreateUI(const Config& config, const os::SystemData& data) -> Element { + Element infoBox = CreateInfoBox(config, data); + + return hbox({ infoBox, filler() }); + } +} // namespace ui diff --git a/src/ui/ui.hpp b/src/ui/ui.hpp new file mode 100644 index 0000000..567a872 --- /dev/null +++ b/src/ui/ui.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include // ftxui::Element +#include // ftxui::Color + +#include "src/config/config.hpp" +#include "src/core/system_data.hpp" +#include "src/util/types.hpp" + +namespace ui { + struct Theme { + ftxui::Color::Palette16 icon; + ftxui::Color::Palette16 label; + ftxui::Color::Palette16 value; + ftxui::Color::Palette16 border; + }; + + extern const Theme DEFAULT_THEME; + + struct Icons { + util::types::StringView user; + util::types::StringView palette; + util::types::StringView calendar; + util::types::StringView host; + util::types::StringView kernel; + util::types::StringView os; + util::types::StringView memory; + util::types::StringView weather; + util::types::StringView music; + util::types::StringView disk; + util::types::StringView shell; + util::types::StringView package; + util::types::StringView desktop; + util::types::StringView windowManager; + }; + + extern const Icons ICON_TYPE; + + /** + * @brief Creates the main UI element based on system data and configuration. + * @param config The application configuration. + * @param data The collected system data. @return The root ftxui::Element for rendering. + */ + fn CreateUI(const Config& config, const os::SystemData& data) -> ftxui::Element; +} // namespace ui diff --git a/src/util/error.hpp b/src/util/error.hpp index 29a5312..ebefa2c 100644 --- a/src/util/error.hpp +++ b/src/util/error.hpp @@ -12,7 +12,7 @@ #include "src/util/types.hpp" -#include "include/matchit.h" +#include "include/matchit.hpp" namespace util { namespace error { @@ -72,12 +72,13 @@ namespace util { is | or_(no_such_file_or_directory, not_a_directory, is_a_directory, file_exists) = NotFound, is | permission_denied = PermissionDenied, is | timed_out = Timeout, - is | _ = errc.category() == std::generic_category() ? InternalError : PlatformSpecific + is | _ = errc.category() == std::generic_category() ? InternalError : PlatformSpecific ); } #ifdef _WIN32 - explicit DracError(const winrt::hresult_error& e) : message(winrt::to_string(e.message())) { + explicit DracError(const winrt::hresult_error& e) + : message(winrt::to_string(e.message())) { using namespace matchit; using enum DracErrorCode; diff --git a/src/util/logging.hpp b/src/util/logging.hpp index c534610..2774919 100644 --- a/src/util/logging.hpp +++ b/src/util/logging.hpp @@ -74,7 +74,10 @@ namespace util::logging { * @enum LogLevel * @brief Represents different log levels. */ - enum class LogLevel : u8 { Debug, Info, Warn, Error }; + enum class LogLevel : u8 { Debug, + Info, + Warn, + Error }; /** * @brief Directly applies ANSI color codes to text @@ -227,7 +230,8 @@ namespace util::logging { #ifdef __cpp_lib_print std::print("\n{}", Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR))); #else - std::cout << '\n' << Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR)); + std::cout << '\n' + << Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR)); #endif #endif diff --git a/src/util/types.hpp b/src/util/types.hpp index c4c0f19..6e87e46 100644 --- a/src/util/types.hpp +++ b/src/util/types.hpp @@ -131,6 +131,7 @@ namespace util::types { MediaInfo() = default; - MediaInfo(Option title, Option artist) : title(std::move(title)), artist(std::move(artist)) {} + MediaInfo(Option title, Option artist) + : title(std::move(title)), artist(std::move(artist)) {} }; } // namespace util::types diff --git a/src/wrappers/dbus.hpp b/src/wrappers/dbus.hpp index 8daa4c9..66d7b3c 100644 --- a/src/wrappers/dbus.hpp +++ b/src/wrappers/dbus.hpp @@ -27,7 +27,10 @@ namespace dbus { bool m_isInitialized = false; public: - Error() : m_isInitialized(true) { dbus_error_init(&m_err); } + Error() + : m_isInitialized(true) { + dbus_error_init(&m_err); + } ~Error() { if (m_isInitialized) @@ -37,7 +40,8 @@ namespace dbus { Error(const Error&) = delete; fn operator=(const Error&)->Error& = delete; - Error(Error&& other) noexcept : m_err(other.m_err), m_isInitialized(other.m_isInitialized) { + Error(Error&& other) noexcept + : m_err(other.m_err), m_isInitialized(other.m_isInitialized) { other.m_isInitialized = false; dbus_error_init(&other.m_err); } @@ -60,30 +64,40 @@ namespace dbus { * @brief Checks if the D-Bus error is set. * @return True if an error is set, false otherwise. */ - [[nodiscard]] fn isSet() const -> bool { return m_isInitialized && dbus_error_is_set(&m_err); } + [[nodiscard]] fn isSet() const -> bool { + return m_isInitialized && dbus_error_is_set(&m_err); + } /** * @brief Gets the error message. * @return The error message string, or "" if not set or not initialized. */ - [[nodiscard]] fn message() const -> const char* { return isSet() ? m_err.message : ""; } + [[nodiscard]] fn message() const -> const char* { + return isSet() ? m_err.message : ""; + } /** * @brief Gets the error name. * @return The error name string (e.g., "org.freedesktop.DBus.Error.Failed"), or "" if not set or not initialized. */ - [[nodiscard]] fn name() const -> const char* { return isSet() ? m_err.name : ""; } + [[nodiscard]] fn name() const -> const char* { + return isSet() ? m_err.name : ""; + } /** * @brief Gets a pointer to the underlying DBusError. Use with caution. * @return Pointer to the DBusError struct. */ - [[nodiscard]] fn get() -> DBusError* { return &m_err; } + [[nodiscard]] fn get() -> DBusError* { + return &m_err; + } /** * @brief Gets a const pointer to the underlying DBusError. * @return Const pointer to the DBusError struct. */ - [[nodiscard]] fn get() const -> const DBusError* { return &m_err; } + [[nodiscard]] fn get() const -> const DBusError* { + return &m_err; + } /** * @brief Converts the D-Bus error to a DraconisError. @@ -107,7 +121,8 @@ namespace dbus { DBusMessageIter m_iter {}; bool m_isValid = false; - explicit MessageIter(const DBusMessageIter& iter, const bool isValid) : m_iter(iter), m_isValid(isValid) {} + explicit MessageIter(const DBusMessageIter& iter, const bool isValid) + : m_iter(iter), m_isValid(isValid) {} friend class Message; @@ -131,7 +146,9 @@ namespace dbus { /** * @brief Checks if the iterator is validly initialized. */ - [[nodiscard]] fn isValid() const -> bool { return m_isValid; } + [[nodiscard]] fn isValid() const -> bool { + return m_isValid; + } /** * @brief Gets the D-Bus type code of the current argument. @@ -154,7 +171,9 @@ namespace dbus { * @brief Advances the iterator to the next argument. * @return True if successful (moved to a next element), false if at the end or iterator is invalid. */ - fn next() -> bool { return m_isValid && dbus_message_iter_next(&m_iter); } + fn next() -> bool { + return m_isValid && dbus_message_iter_next(&m_iter); + } /** * @brief Recurses into a container-type argument (e.g., array, struct, variant). @@ -197,7 +216,8 @@ namespace dbus { DBusMessage* m_msg = nullptr; public: - explicit Message(DBusMessage* msg = nullptr) : m_msg(msg) {} + explicit Message(DBusMessage* msg = nullptr) + : m_msg(msg) {} ~Message() { if (m_msg) @@ -207,7 +227,8 @@ namespace dbus { Message(const Message&) = delete; fn operator=(const Message&)->Message& = delete; - Message(Message&& other) noexcept : m_msg(std::exchange(other.m_msg, nullptr)) {} + Message(Message&& other) noexcept + : m_msg(std::exchange(other.m_msg, nullptr)) {} fn operator=(Message&& other) noexcept -> Message& { if (this != &other) { @@ -222,7 +243,9 @@ namespace dbus { * @brief Gets the underlying DBusMessage pointer. Use with caution. * @return The raw DBusMessage pointer, or nullptr if not holding a message. */ - [[nodiscard]] fn get() const -> DBusMessage* { return m_msg; } + [[nodiscard]] fn get() const -> DBusMessage* { + return m_msg; + } /** * @brief Initializes a message iterator for reading arguments from this message. @@ -296,7 +319,8 @@ namespace dbus { DBusConnection* m_conn = nullptr; public: - explicit Connection(DBusConnection* conn = nullptr) : m_conn(conn) {} + explicit Connection(DBusConnection* conn = nullptr) + : m_conn(conn) {} ~Connection() { if (m_conn) @@ -306,7 +330,8 @@ namespace dbus { Connection(const Connection&) = delete; fn operator=(const Connection&)->Connection& = delete; - Connection(Connection&& other) noexcept : m_conn(std::exchange(other.m_conn, nullptr)) {} + Connection(Connection&& other) noexcept + : m_conn(std::exchange(other.m_conn, nullptr)) {} fn operator=(Connection&& other) noexcept -> Connection& { if (this != &other) { @@ -322,7 +347,9 @@ namespace dbus { * @brief Gets the underlying DBusConnection pointer. Use with caution. * @return The raw DBusConnection pointer, or nullptr if not holding a connection. */ - [[nodiscard]] fn get() const -> DBusConnection* { return m_conn; } + [[nodiscard]] fn get() const -> DBusConnection* { + return m_conn; + } /** * @brief Sends a message and waits for a reply, blocking execution. diff --git a/src/wrappers/wayland.hpp b/src/wrappers/wayland.hpp index 384be68..9bc8317 100644 --- a/src/wrappers/wayland.hpp +++ b/src/wrappers/wayland.hpp @@ -16,9 +16,15 @@ namespace wl { using display = wl_display; // NOLINTBEGIN(readability-identifier-naming) - inline fn connect(const char* name) -> display* { return wl_display_connect(name); } - inline fn disconnect(display* display) -> void { wl_display_disconnect(display); } - inline fn get_fd(display* display) -> int { return wl_display_get_fd(display); } + inline fn connect(const char* name) -> display* { + return wl_display_connect(name); + } + inline fn disconnect(display* display) -> void { + wl_display_disconnect(display); + } + inline fn get_fd(display* display) -> int { + return wl_display_get_fd(display); + } // NOLINTEND(readability-identifier-naming) /** @@ -77,7 +83,8 @@ namespace wl { fn operator=(const DisplayGuard&)->DisplayGuard& = delete; // Movable - DisplayGuard(DisplayGuard&& other) noexcept : m_display(std::exchange(other.m_display, nullptr)) {} + DisplayGuard(DisplayGuard&& other) noexcept + : m_display(std::exchange(other.m_display, nullptr)) {} fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& { if (this != &other) { if (m_display) @@ -89,10 +96,16 @@ namespace wl { return *this; } - [[nodiscard]] explicit operator bool() const { return m_display != nullptr; } + [[nodiscard]] explicit operator bool() const { + return m_display != nullptr; + } - [[nodiscard]] fn get() const -> display* { return m_display; } - [[nodiscard]] fn fd() const -> util::types::i32 { return get_fd(m_display); } + [[nodiscard]] fn get() const -> display* { + return m_display; + } + [[nodiscard]] fn fd() const -> util::types::i32 { + return get_fd(m_display); + } }; } // namespace wl diff --git a/src/wrappers/xcb.hpp b/src/wrappers/xcb.hpp index c61b26d..521c783 100644 --- a/src/wrappers/xcb.hpp +++ b/src/wrappers/xcb.hpp @@ -53,8 +53,12 @@ namespace xcb { inline fn connect(const char* displayname, int* screenp) -> connection_t* { return xcb_connect(displayname, screenp); } - inline fn disconnect(connection_t* conn) -> void { xcb_disconnect(conn); } - inline fn connection_has_error(connection_t* conn) -> int { return xcb_connection_has_error(conn); } + inline fn disconnect(connection_t* conn) -> void { + xcb_disconnect(conn); + } + inline fn connection_has_error(connection_t* conn) -> int { + return xcb_connection_has_error(conn); + } inline fn intern_atom(connection_t* conn, const uint8_t only_if_exists, const uint16_t name_len, const char* name) -> intern_atom_cookie_t { return xcb_intern_atom(conn, only_if_exists, name_len, name); @@ -81,7 +85,9 @@ namespace xcb { inline fn get_property_value_length(const get_property_reply_t* reply) -> int { return xcb_get_property_value_length(reply); } - inline fn get_property_value(const get_property_reply_t* reply) -> void* { return xcb_get_property_value(reply); } + inline fn get_property_value(const get_property_reply_t* reply) -> void* { + return xcb_get_property_value(reply); + } // NOLINTEND(readability-identifier-naming) /** @@ -96,7 +102,8 @@ namespace xcb { * Opens an XCB connection * @param name Display name (nullptr for default) */ - explicit DisplayGuard(const util::types::CStr name = nullptr) : m_connection(connect(name, nullptr)) {} + explicit DisplayGuard(const util::types::CStr name = nullptr) + : m_connection(connect(name, nullptr)) {} ~DisplayGuard() { if (m_connection) disconnect(m_connection); @@ -107,7 +114,8 @@ namespace xcb { fn operator=(const DisplayGuard&)->DisplayGuard& = delete; // Movable - DisplayGuard(DisplayGuard&& other) noexcept : m_connection(std::exchange(other.m_connection, nullptr)) {} + DisplayGuard(DisplayGuard&& other) noexcept + : m_connection(std::exchange(other.m_connection, nullptr)) {} fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& { if (this != &other) { if (m_connection) @@ -118,11 +126,17 @@ namespace xcb { return *this; } - [[nodiscard]] explicit operator bool() const { return m_connection && !connection_has_error(m_connection); } + [[nodiscard]] explicit operator bool() const { + return m_connection && !connection_has_error(m_connection); + } - [[nodiscard]] fn get() const -> connection_t* { return m_connection; } + [[nodiscard]] fn get() const -> connection_t* { + return m_connection; + } - [[nodiscard]] fn setup() const -> const setup_t* { return m_connection ? xcb_get_setup(m_connection) : nullptr; } + [[nodiscard]] fn setup() const -> const setup_t* { + return m_connection ? xcb_get_setup(m_connection) : nullptr; + } [[nodiscard]] fn rootScreen() const -> screen_t* { const setup_t* setup = this->setup(); @@ -140,7 +154,8 @@ namespace xcb { public: ReplyGuard() = default; - explicit ReplyGuard(T* reply) : m_reply(reply) {} + explicit ReplyGuard(T* reply) + : m_reply(reply) {} ~ReplyGuard() { if (m_reply) @@ -152,7 +167,8 @@ namespace xcb { fn operator=(const ReplyGuard&)->ReplyGuard& = delete; // Movable - ReplyGuard(ReplyGuard&& other) noexcept : m_reply(std::exchange(other.m_reply, nullptr)) {} + ReplyGuard(ReplyGuard&& other) noexcept + : m_reply(std::exchange(other.m_reply, nullptr)) {} fn operator=(ReplyGuard&& other) noexcept -> ReplyGuard& { if (this != &other) { if (m_reply) @@ -163,11 +179,19 @@ namespace xcb { return *this; } - [[nodiscard]] explicit operator bool() const { return m_reply != nullptr; } + [[nodiscard]] explicit operator bool() const { + return m_reply != nullptr; + } - [[nodiscard]] fn get() const -> T* { return m_reply; } - [[nodiscard]] fn operator->() const->T* { return m_reply; } - [[nodiscard]] fn operator*() const->T& { return *m_reply; } + [[nodiscard]] fn get() const -> T* { + return m_reply; + } + [[nodiscard]] fn operator->() const->T* { + return m_reply; + } + [[nodiscard]] fn operator*() const->T& { + return *m_reply; + } }; } // namespace xcb