diff --git a/lib/mu-config.cc b/lib/mu-config.cc index 6ce5c845..573a4dac 100644 --- a/lib/mu-config.cc +++ b/lib/mu-config.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2023 Dirk-Jan C. Binnema +** Copyright (C) 2023-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -26,7 +26,6 @@ validate_props() { size_t id{0}; for (auto&& prop: Config::properties) { - // ids must match if (static_cast(prop.id) != id) return false; @@ -79,7 +78,7 @@ test_basic() } { - g_assert_true(Config::property().default_val == "50000"); + g_assert_true(!!Config::property().default_val); g_assert_cmpuint(conf_db.get(),==,50000); assert_valid_result(conf_db.set(123456)); diff --git a/lib/mu-config.hh b/lib/mu-config.hh index bf461d85..8647b6b1 100644 --- a/lib/mu-config.hh +++ b/lib/mu-config.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2023-2025 Dirk-Jan C. Binnema +** Copyright (C) 2023-2026 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the @@ -31,42 +31,54 @@ #include "mu-xapian-db.hh" +#include +#include + #include #include #include +#include + namespace Mu { struct Property { - enum struct Id { - BatchSize, /**< Xapian batch-size */ - Contacts, /**< Cache of contact information */ - Created, /**< Time of creation */ - IgnoredAddresses, /**< Email addresses ignored for the contacts-cache */ - Labels, /**< Serialized label information. */ - LastChange, /**< Time of last change */ - LastIndex, /**< Time of last index */ - MaxMessageSize, /**< Maximum message size (in bytes) */ - PersonalAddresses, /**< List of personal e-mail addresses */ - RootMaildir, /**< Root maildir path */ - SchemaVersion, /**< Xapian DB schema version */ - SupportNgrams, /**< Support ngrams for indexing & querying - * for e.g. CJK languages */ - /* */ - _count_ /* Number of Ids */ + enum struct Id { + // store properties, i.e. properties associated with + // some store object (and store in the database), either + // at init time or updated during runtime. + BatchSize, /**< Xapian batch-size */ + Contacts, /**< Cache of contact information */ + Created, /**< Time of creation */ + IgnoredAddresses, /**< Email addresses ignored for the + contacts-cache */ + Labels, /**< Serialized label information. */ + LastChange, /**< Time of last change */ + LastIndex, /**< Time of last index */ + MaxMessageSize, /**< Maximum message size (in bytes) */ + PersonalAddresses, /**< List of personal e-mail addresses */ + RootMaildir, /**< Root maildir path */ + SchemaVersion, /**< Xapian DB schema version */ + NgramsEnabled, /**< Support ngrams for indexing & querying + * for e.g. CJK languages */ + // build/system properties + MuVersion, /**< Mu version */ + XapianVersion, /**< Xapian runtime version */ + GlibVersion, /**< GLib version */ + GmimeVersion, /**< GMime version */ + ScmEnabled, /**< Built with SCM (Guile) support */ + LanguageEnabled /**< Built with language-detection support */ }; - static constexpr size_t id_size = static_cast(Id::_count_); - /**< Number of Property::Ids */ - - enum struct Flags { - None = 0, /**< Nothing in particular */ - ReadOnly = 1 << 0, /**< Property is read-only for external use - * (but can change from within the store) */ - Configurable = 1 << 1, /**< A user-configurable parameter; name - * starts with 'conf-' */ - Internal = 1 << 2, /**< Mu-internal field */ - Runtime = 1 << 3, /**< May change at runtime */ + enum struct Flags { + None = 0, /**< Nothing in particular */ + ReadOnly = 1 << 0, /**< Property is read-only for external use + * (but can change from within the store) */ + Configurable = 1 << 1, /**< A user-configurable parameter; name + * starts with 'conf-' */ + Internal = 1 << 2, /**< Mu-internal field */ + Runtime = 1 << 3, /**< May change at runtime */ + System = 1 << 4, /**< System property; read-only */ }; enum struct Type { Boolean, /**< Some boolean value */ @@ -78,12 +90,13 @@ struct Property { }; using Value = std::variant >; + using DefaultValue = std::string(*)(); /**< Function that returns a string_view */ Id id; Type type; Flags flags; std::string_view name; - std::string_view default_val; + DefaultValue default_val; std::string_view description; }; @@ -96,14 +109,17 @@ public: using Flags = Property::Flags; using Value = Property::Value; - static constexpr std::array - properties = {{ + /** + * Properties specific to the store. + */ + static constexpr auto properties = std::to_array({ + // store-properties { Id::BatchSize, Type::Number, Flags::Configurable, "batch-size", - "50000", + []()->std::string {return "50000";}, "Maximum number of changes in a database transaction" }, { @@ -131,7 +147,6 @@ public: "E-mail addresses ignored for the contacts-cache, " "literal or /regexp/" }, - { Id::Labels, Type::String, @@ -162,7 +177,7 @@ public: Type::Number, Flags::Configurable, "max-message-size", - "100000000", // default max: 100M bytes + []()->std::string {return "100000000";}, // default max: 100M bytes "Maximum message size (in bytes); bigger messages are skipped" }, { @@ -190,14 +205,66 @@ public: "Version of the Xapian database schema" }, { - Id::SupportNgrams, + Id::NgramsEnabled, Type::Boolean, Flags::Configurable, - "support-ngrams", + "ngrams-enabled", {}, "Support n-grams for working with CJK and other languages" }, - }}; + + // system properties + { + Id::MuVersion, + Type::String, + Flags::System, + "mu-version", + []()->std::string {return VERSION;}, + "mu version string" + }, + { + Id::XapianVersion, + Type::String, + Flags::System, + "xapian-version", + []()->std::string {return Xapian::version_string();}, + "Xapian runtime version string" + }, + { + Id::GlibVersion, + Type::String, + Flags::System, + "glib-version", + []()->std::string{return mu_format("{}.{}.{}", glib_major_version, glib_minor_version, + glib_micro_version);}, + "GLib runtime version string" + }, + { + Id::GmimeVersion, + Type::String, + Flags::System, + "gmime-version", + []()->std::string {return mu_format("{}.{}.{}", gmime_major_version, gmime_minor_version, + gmime_micro_version);}, + "GMime runtime version string" + }, + { + Id::ScmEnabled, + Type::Boolean, + Flags::System, + "scm-enabled", + []()->std::string { return mu_format("{}", BUILD_GUILE); }, + "Is SCM/Guile supported?" + }, + { + Id::LanguageEnabled, + Type::Boolean, + Flags::System, + "language-enabled", + []()->std::string { return mu_format("{}", HAVE_CLD2); }, + "Is language detection/searching supported?" + }, + }); /** * Construct a new Config object. @@ -210,7 +277,7 @@ public: /** * Get the property by its id * - * @param id a property id (!= Id::_count_) + * @param id a property id * * @return the property */ @@ -235,34 +302,19 @@ public: } - /** - * Get the string-value for prop. - * - * For internal use - * - * @param prop some property - * - * @return a string - */ - std::string get_str(const Property& prop) const { - const auto str = cstore_.metadata(std::string{prop.name}); - return str.empty() ? std::string{prop.default_val} : str; - } - - /** * Get the property value decoded based on the type * - * @param prop_id a property id + * @param str the raw string value * - * @return the value or Nothing + * @return the value */ template - static constexpr auto decode(const std::string& str) { + static auto decode(const std::string& str) { if constexpr (type == Type::Number) return static_cast(str.empty() ? 0 : std::atoll(str.c_str())); - if constexpr (type == Type::Boolean) - return static_cast(str.empty() ? false : + else if constexpr (type == Type::Boolean) + return static_cast(str.empty() ? false : std::atol(str.c_str()) != 0); else if constexpr (type == Type::Timestamp) return static_cast(str.empty() ? 0 : std::atoll(str.c_str())); @@ -273,6 +325,62 @@ public: throw std::logic_error("invalid type"); } + /** + * Get default value for the property, or Nothing if it does not have one. + * + * @param prop some property + * + * @return string or nothing + */ + static Option default_value(const Property& prop) { + if (!prop.default_val) + return Nothing; + else + return prop.default_val(); + } + + /** + * Get the raw string-value for some property + * + * @param prop some property + * + * @return a string + */ + std::string as_raw_string(const Property& prop) const { + // system properties are _not_ stored in the db. + if (any_of(prop.flags & Property::Flags::System)) { + return default_value(prop).value_or(""); + } else { + if (const auto str = cstore_.metadata(std::string{prop.name}); !str.empty()) + return str; + else + return default_value(prop).value_or(""); + } + } + + /** + * Get the string-value for some property (for display) + * + * @param prop some property + * + * @return a string + */ + std::string as_display_string(const Property& prop) const { + // system properties are _not_ stored in the db. + const auto str{as_raw_string(prop)}; + switch (prop.type) { + case Type::Boolean: + return decode(str) ? "yes" : "no"; + case Type::StringList: + return mu_format("{}", mu_join(decode(str), ", ")); + case Type::Timestamp: { + const auto t{decode(str)}; + return t == 0 ? "never" : mu_format("{:%c}", mu_time(t)); + } + default: + return str; + } + } /** @@ -285,7 +393,7 @@ public: template auto get() const { constexpr auto& prop{property()}; - return decode(get_str(prop)); + return decode(as_raw_string(prop)); } /** @@ -299,9 +407,10 @@ public: template Result set(const T& val) { constexpr auto&& prop{property()}; + if (any_of(prop.flags & Property::Flags::System)) + return Err(Error::Code::AccessDenied, "cannot write system property"); if (read_only()) - return Err(Error::Code::AccessDenied, - "cannot write to read-only db"); + return Err(Error::Code::AccessDenied, "cannot write to read-only db"); const auto strval = std::invoke([&]{ if constexpr (prop.type == Type::Number || prop.type == Type::Timestamp) @@ -337,7 +446,7 @@ public: for (auto&& prop: properties) { if (any_of(prop.flags & Flags::Configurable)) { const auto&& key{std::string{prop.name}}; - if (auto&& val{src.cstore_.metadata(key)}; !val.empty()) + if (const auto& val{src.cstore_.metadata(key)}; !val.empty()) cstore_.set_metadata(key, std::string{val}); } } diff --git a/lib/mu-indexer.cc b/lib/mu-indexer.cc index 7ea94d9a..24d2e478 100644 --- a/lib/mu-indexer.cc +++ b/lib/mu-indexer.cc @@ -95,7 +95,7 @@ struct Indexer::Private { store.root_maildir(), store.path(), store.config().get(), was_empty_, - store.config().get()); + store.config().get()); } ~Private() { diff --git a/lib/mu-store.cc b/lib/mu-store.cc index 64604748..51cee6e1 100644 --- a/lib/mu-store.cc +++ b/lib/mu-store.cc @@ -110,8 +110,8 @@ struct Store::Private { } Message::Options make_message_options(const Config& conf) { - if (conf.get()) - return Message::Options::SupportNgrams; + if (conf.get()) + return Message::Options::NgramsEnabled; else return Message::Options::None; } @@ -369,8 +369,8 @@ Store::add_message(Message& msg, bool is_new) return Err(res.error()); // we shouldn't mix ngrams/non-ngrams messages. - if (any_of(msg.options() & Message::Options::SupportNgrams) != - any_of(message_options() & Message::Options::SupportNgrams)) + if (any_of(msg.options() & Message::Options::NgramsEnabled) != + any_of(message_options() & Message::Options::NgramsEnabled)) return Err(Error::Code::InvalidArgument, "incompatible message options"); /* add contacts from this message to cache; this cache diff --git a/lib/tests/test-mu-store-query.cc b/lib/tests/test-mu-store-query.cc index 586f098c..6bff871e 100644 --- a/lib/tests/test-mu-store-query.cc +++ b/lib/tests/test-mu-store-query.cc @@ -1043,7 +1043,7 @@ https://trac.xapian.org/ticket/719 MemDb mdb; Config conf{mdb}; - conf.set(true); + conf.set(true); TempDir tdir; auto store{make_test_store(tdir.path(), test_msgs, conf)}; diff --git a/meson.build b/meson.build index 1b44edee..898c85c6 100644 --- a/meson.build +++ b/meson.build @@ -259,6 +259,7 @@ if not get_option('cld2').disabled() and cld2_dep.found() config_h_data.set('HAVE_CLD2', 1) else message('CLD2 not found or disabled; no support for language detection') + config_h_data.set('HAVE_CLD2', 0) endif # @@ -339,6 +340,8 @@ if guile_dep.found() if not get_option('scm').disabled() config_h_data.set('BUILD_SCM', 1) subdir('scm') + else + config_h_data.set('BUILD_SCM', 0) endif # old-style guile-support (deprecated) diff --git a/mu/mu-cmd-info.cc b/mu/mu-cmd-info.cc index b52fef90..219f1415 100644 --- a/mu/mu-cmd-info.cc +++ b/mu/mu-cmd-info.cc @@ -210,35 +210,28 @@ topic_flags(const Options& opts) static Result topic_store(const Mu::Store& store, const Options& opts) { - auto tstamp = [](::time_t t)->std::string { - if (t == 0) - return "never"; - else - return mu_format("{:%c}", mu_time(t)); - }; - Table info; const auto conf{store.config()}; - info.add_row({"property", "value"}); - info.add_row({"maildir", store.root_maildir()}); - info.add_row({"database-path", store.path()}); - info.add_row({"schema-version", - mu_format("{}", conf.get())}); - info.add_row({"max-message-size", mu_format("{}", conf.get())}); - info.add_row({"batch-size", mu_format("{}", conf.get())}); - info.add_row({"created", tstamp(conf.get())}); - - for (auto&& c : conf.get()) - info.add_row({"personal-address", c}); - for (auto&& c : conf.get()) - info.add_row({"ignored-address", c}); - - info.add_row({"messages in store", mu_format("{}", store.size())}); - info.add_row({"support-ngrams", conf.get() ? "yes" : "no"}); - - info.add_row({"last-change", tstamp(store.statistics().last_change)}); - info.add_row({"last-index", tstamp(store.statistics().last_index)}); - + info.add_row({"property", "value", "description"}); + info.add_row({"database-path", store.path(), "Path to xapian database"}); + info.add_row({"message-number", mu_format("{}", store.size()), + "Number of messages in store"}); + for (const auto& prop: Config::properties) { + if (any_of(prop.flags & (Property::Flags::System|Property::Flags::Internal))) + continue; + switch(prop.id) { + case Config::Id::PersonalAddresses: + case Config::Id::IgnoredAddresses: { + const auto addrs{conf.decode(conf.as_raw_string(prop))}; + for (auto& addr: addrs) { + info.add_row({std::string{prop.name}, addr, std::string{prop.description}}); + } + } break; + default: + info.add_row({std::string{prop.name},conf.as_display_string(prop), + std::string{prop.description}}); + } + } if (!opts.nocolor) colorify(info, opts); @@ -257,58 +250,19 @@ topic_maildirs(const Mu::Store& store, const Options& opts) } static Result -topic_mu(const Options& opts) +topic_mu(const Mu::Store& store, const Options& opts) { - Table info; - using namespace tabulate; + Table info; + const auto conf{store.config()}; info.add_row({"property", "value", "description"}); - info.add_row({"mu-version", std::string{VERSION}, "Mu runtime version"}); - info.add_row({"xapian-version", Xapian::version_string(), "Xapian runtime version"}); - info.add_row({"gmime-version", - mu_format("{}.{}.{}", gmime_major_version, gmime_minor_version, - gmime_micro_version), "GMime runtime version"}); - info.add_row({"glib-version", - mu_format("{}.{}.{}", glib_major_version, glib_minor_version, - glib_micro_version), "GLib runtime version"}); - info.add_row({"schema-version", mu_format("{}", MU_STORE_SCHEMA_VERSION), - "Version of mu's database schema"}); - - info.add_row({"cld2-support", -#if HAVE_CLD2 - "yes" -#else - "no" -#endif - , "Support searching by language-code?"}); - - info.add_row({"guile-support", -#if BUILD_GUILE - "yes" -#else - "no" -#endif /*BUILD_GUILE*/ - , "GNU Guile 3.x support (old)?"}); - - info.add_row({"scm-support", -#if BUILD_SCM - "yes" -#else - "no" -#endif /*BUILD_SCM*/ - , "GNU Guile 3.x support (new)?"}); - - - - - info.add_row({"readline-support", -#if HAVE_LIBREADLINE - "yes" -#else - "no" -#endif - , "Better 'mu server' REPL for debugging"}); + for (const auto& prop: Config::properties) { + if (any_of(prop.flags & Property::Flags::System)) { + info.add_row({std::string{prop.name}, conf.as_display_string(prop), + std::string{prop.description}}); + } + } if (!opts.nocolor) colorify(info, opts); @@ -337,9 +291,9 @@ Mu::mu_cmd_info(const Mu::Store& store, const Options& opts) std::cout << std::endl; topic_flags(opts); } else if (topic == "mu") { - return topic_mu(opts); + return topic_mu(store, opts); } else { - topic_mu(opts); + topic_mu(store, opts); MaybeAnsi col{!opts.nocolor}; using Color = MaybeAnsi::Color; diff --git a/mu/mu-cmd-init.cc b/mu/mu-cmd-init.cc index 8463ce7b..b36f3235 100644 --- a/mu/mu-cmd-init.cc +++ b/mu/mu-cmd-init.cc @@ -61,7 +61,7 @@ Mu::mu_cmd_init(const Options& opts) if (!opts.init.ignored_addresses.empty()) conf.set(opts.init.ignored_addresses); if (opts.init.support_ngrams) - conf.set(true); + conf.set(true); return Store::make_new(opts.runtime_path(RuntimePath::XapianDb), opts.init.maildir, conf); diff --git a/scm/mu-scm-store.cc b/scm/mu-scm-store.cc index cf74eafb..25d1cc2d 100644 --- a/scm/mu-scm-store.cc +++ b/scm/mu-scm-store.cc @@ -56,7 +56,7 @@ subr_cc_store_alist(SCM store_scm) try { (MuConfig::Flags::Internal | MuConfig::Flags::Runtime))) continue; - const auto str{conf.get_str(prop)}; + const auto str{conf.as_display_string(prop)}; if (str.empty()) continue;