feat(xkeyboard): Icon matching using variant (#2521)

* feat(string_util): add contains_nocase

* feat(xkeyboard): Enable icon by variant

* Cleanup

* string_util: add some cases to string test

* string_util: rename contains_nocase -> contains_ignore_case

* layouticonset: use contains_ignore_case

* layouticonset: apply renamings and remove dead code

* remove VARIANT_NONE and use empty string instead

* use emplace_back and add assert

* layouticonset: precompute condition

* xkeyboard: restore missing continue

* Cleanup parse_icons

* Always choose the first case-insensitive match

* Cleanup layouticonset.get

* update the changelog

Co-authored-by: patrick96 <p.ziegler96@gmail.com>
This commit is contained in:
dvermd 2021-10-05 12:12:47 +02:00 committed by GitHub
parent a2968127d1
commit 98dffc292a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 302 additions and 16 deletions

View File

@ -122,6 +122,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `double-click-interval` setting to the bar section to control the time
interval in which a double-click is recognized. Defaults to 400 (ms)
([`#1441`](https://github.com/polybar/polybar/issues/1441))
- `internal/xkeyboard`: Allow configuring icons using variant
([`#2414`](https://github.com/polybar/polybar/issues/2414))
### Changed
- We rewrote polybar's main event loop. This shouldn't change any behavior for

View File

@ -0,0 +1,33 @@
#pragma once
#include <tuple>
#include <vector>
#include "common.hpp"
#include "drawtypes/label.hpp"
#include "utils/mixins.hpp"
using std::tuple;
POLYBAR_NS
namespace drawtypes {
class layouticonset : public non_copyable_mixin<layouticonset> {
public:
explicit layouticonset(label_t&& default_icon);
bool add(const string& layout, const string& variant, label_t&& icon);
label_t get(const string& layout, const string& variant) const;
bool contains(const string& layout, const string& variant) const;
static constexpr const char* VARIANT_ANY = "_";
protected:
label_t m_default_icon;
vector<tuple<string, string, label_t>> m_layout_icons;
};
using layouticonset_t = shared_ptr<layouticonset>;
} // namespace drawtypes
POLYBAR_NS_END

View File

@ -3,6 +3,7 @@
#include "common.hpp"
#include "components/config.hpp"
#include "components/types.hpp"
#include "drawtypes/layouticonset.hpp"
#include "modules/meta/event_handler.hpp"
#include "modules/meta/static_module.hpp"
#include "x11/extensions/xkb.hpp"
@ -40,6 +41,9 @@ namespace modules {
void action_switch();
void define_layout_icon(const string& entry, const string& layout, const string& variant, label_t&& icon);
void parse_icons();
private:
static constexpr const char* TAG_LABEL_LAYOUT{"<label-layout>"};
static constexpr const char* TAG_LABEL_INDICATOR{"<label-indicator>"};
@ -61,7 +65,7 @@ namespace modules {
map<keyboard::indicator::type, label_t> m_indicator_off_labels;
vector<string> m_blacklist;
iconset_t m_layout_icons;
layouticonset_t m_layout_icons;
iconset_t m_indicator_icons_on;
iconset_t m_indicator_icons_off;
};

View File

@ -62,6 +62,7 @@ namespace string_util {
using hash_type = unsigned long;
bool contains(const string& haystack, const string& needle);
bool contains_ignore_case(const string& haystack, const string& needle);
string upper(const string& s);
string lower(const string& s);
bool compare(const string& s1, const string& s2);
@ -96,7 +97,8 @@ namespace string_util {
string floating_point(double value, size_t precision, bool fixed = false, const string& locale = "");
string filesize_mib(unsigned long long kibibytes, size_t precision = 0, const string& locale = "");
string filesize_gib(unsigned long long kibibytes, size_t precision = 0, const string& locale = "");
string filesize_gib_mib(unsigned long long kibibytes, size_t precision_mib = 0, size_t precision_gib = 0, const string& locale = "");
string filesize_gib_mib(
unsigned long long kibibytes, size_t precision_mib = 0, size_t precision_gib = 0, const string& locale = "");
string filesize(unsigned long long kbytes, size_t precision = 0, bool fixed = false, const string& locale = "");
hash_type hash(const string& src);

View File

@ -72,6 +72,7 @@ if(BUILD_LIBPOLY)
${src_dir}/drawtypes/animation.cpp
${src_dir}/drawtypes/iconset.cpp
${src_dir}/drawtypes/layouticonset.cpp
${src_dir}/drawtypes/label.cpp
${src_dir}/drawtypes/progressbar.cpp
${src_dir}/drawtypes/ramp.cpp

View File

@ -0,0 +1,99 @@
#include "drawtypes/layouticonset.hpp"
POLYBAR_NS
namespace drawtypes {
layouticonset::layouticonset(label_t&& default_icon) : m_default_icon(default_icon) {}
bool layouticonset::add(const string& layout, const string& variant, label_t&& icon) {
if (layout == VARIANT_ANY && variant == VARIANT_ANY) {
return false;
}
m_layout_icons.emplace_back(layout, variant, icon);
return true;
}
label_t layouticonset::get(const string& layout, const string& variant) const {
// The layout, variant are matched against defined icons in that order:
// 1. perfect match on layout and perfect match on variant (ex: us;Colemak;<icon>)
// 2. perfect match on layout and case insensitive search on variant (ex: us;coLEmAk;<icon>)
// 3. perfect match on layout and the any variant '_' (ex: us;<icon> or us;_;<icon>)
// 4. any layout for icon and perfect match on variant (ex: _;Colemak;<icon>)
// 5. any layout for icon and case insensitive search on variant (ex: _;coLEmAk;<icon>)
// 6. no match at all => default icon if defined
/*
* The minimal case that was matched.
* Once a case is matched, this is updated and no case with the same or higher number can be matched again.
*/
int min_case = 6;
// Case 6: initializing with default
label_t icon = m_default_icon;
for (auto it : m_layout_icons) {
const string& icon_layout = std::get<0>(it);
const string& icon_variant = std::get<1>(it);
label_t icon_label = std::get<2>(it);
bool is_variant_match = icon_variant == variant;
bool is_variant_any = icon_variant == VARIANT_ANY;
bool is_variant_match_fuzzy =
!is_variant_any && !icon_variant.empty() && string_util::contains_ignore_case(variant, icon_variant);
// Which of the 6 match cases is matched here.
int current_case = 6;
if (icon_layout == layout) {
if (is_variant_match) {
// Case 1
current_case = 1;
} else if (is_variant_match_fuzzy) {
// Case 2
current_case = 2;
} else if (is_variant_any) {
// Case 3
current_case = 3;
}
} else if (icon_layout == VARIANT_ANY) {
if (is_variant_match) {
// Case 4
current_case = 4;
} else if (is_variant_match_fuzzy) {
// Case 5
current_case = 5;
}
}
/*
* We matched with a higher priority than before -> update icon.
*/
if (current_case < min_case) {
icon = icon_label;
min_case = current_case;
}
if (current_case == 1) {
// Case 1: perfect match, we can break early
break;
}
}
return icon;
}
bool layouticonset::contains(const string& layout, const string& variant) const {
for (auto it : m_layout_icons) {
const string& icon_layout = std::get<0>(it);
const string& icon_variant = std::get<1>(it);
if (icon_layout == layout && icon_variant == variant) {
return true;
}
}
return false;
}
} // namespace drawtypes
POLYBAR_NS_END

View File

@ -44,15 +44,7 @@ namespace modules {
m_blacklist = m_conf.get_list(name(), "blacklist", {});
// load layout icons
m_layout_icons = std::make_shared<iconset>();
m_layout_icons->add(DEFAULT_LAYOUT_ICON, load_optional_label(m_conf, name(), DEFAULT_LAYOUT_ICON, ""s));
for (const auto& it : m_conf.get_list<string>(name(), "layout-icon", {})) {
auto vec = string_util::tokenize(it, ';');
if (vec.size() == 2) {
m_layout_icons->add(vec[0], std::make_shared<label>(vec[1]));
}
}
parse_icons();
// Add formats and elements
m_formatter->add(DEFAULT_FORMAT, FORMAT_DEFAULT, {TAG_LABEL_LAYOUT, TAG_LABEL_INDICATOR});
@ -123,7 +115,9 @@ namespace modules {
m_layout->replace_token("%variant%", m_keyboard->variant_name(m_keyboard->current()));
auto const current_layout = m_keyboard->layout_name(m_keyboard->current());
auto icon = m_layout_icons->get(current_layout, DEFAULT_LAYOUT_ICON);
auto const current_variant = m_keyboard->variant_name(m_keyboard->current());
auto icon = m_layout_icons->get(current_layout, current_variant);
m_layout->replace_token("%icon%", icon->get());
m_layout->replace_token("%layout%", current_layout);
@ -281,6 +275,50 @@ namespace modules {
update();
}
}
void xkeyboard_module::parse_icons() {
m_layout_icons = make_shared<layouticonset>(load_optional_label(m_conf, name(), DEFAULT_LAYOUT_ICON, ""s));
for (const auto& it : m_conf.get_list<string>(name(), "layout-icon", {})) {
auto vec = string_util::tokenize(it, ';');
size_t size = vec.size();
if (size != 2 && size != 3) {
m_log.warn("%s: Malformed layout-icon '%s'", name(), it);
continue;
}
const string& layout = vec[0];
if (layout.empty()) {
m_log.warn("%s: layout-icon '%s' is invalid: there must always be a layout defined", name(), it);
continue;
}
const string& variant = size == 2 ? layouticonset::VARIANT_ANY : vec[1];
const string& icon = vec.back();
if (layout == layouticonset::VARIANT_ANY && variant == layouticonset::VARIANT_ANY) {
m_log.warn("%s: Using '%s' for layout-icon means declaring a default icon, use 'layout-icon-default' instead",
name(), it);
continue;
}
define_layout_icon(it, layout, variant, std::make_shared<label>(icon));
}
}
void xkeyboard_module::define_layout_icon(
const string& entry, const string& layout, const string& variant, label_t&& icon) {
if (m_layout_icons->contains(layout, variant)) {
m_log.warn(
"%s: An equivalent matching is already defined for '%s;%s' => ignoring '%s'", name(), layout, variant, entry);
} else if (!m_layout_icons->add(layout, variant, std::forward<label_t>(icon))) {
m_log.err(
"%s: '%s' cannot be added to internal structure. This case should never happen and must be reported as a bug",
name(), entry);
}
}
} // namespace modules
POLYBAR_NS_END

View File

@ -1,10 +1,10 @@
#include "utils/string.hpp"
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <utility>
#include "utils/string.hpp"
POLYBAR_NS
namespace string_util {
@ -15,6 +15,13 @@ namespace string_util {
return haystack.find(needle) != string::npos;
}
/**
* Check if haystack contains needle ignoring case
*/
bool contains_ignore_case(const string& haystack, const string& needle) {
return lower(haystack).find(lower(needle)) != string::npos;
}
/**
* Convert string to uppercase
*/
@ -294,7 +301,8 @@ namespace string_util {
/**
* Create a GiB string, if the value in GiB is >= 1.0. Otherwise, create a MiB string.
*/
string filesize_gib_mib(unsigned long long kibibytes, size_t precision_mib, size_t precision_gib, const string& locale) {
string filesize_gib_mib(
unsigned long long kibibytes, size_t precision_mib, size_t precision_gib, const string& locale) {
if (kibibytes < 1024 * 1024) {
return filesize_mib(kibibytes, precision_mib, locale);
} else {

View File

@ -63,6 +63,7 @@ add_unit_test(components/config_parser)
add_unit_test(drawtypes/label)
add_unit_test(drawtypes/ramp)
add_unit_test(drawtypes/iconset)
add_unit_test(drawtypes/layouticonset)
add_unit_test(tags/parser)
add_unit_test(tags/dispatch)
add_unit_test(tags/action_context)

View File

@ -0,0 +1,76 @@
#include "drawtypes/layouticonset.hpp"
#include "common/test.hpp"
using namespace std;
using namespace polybar;
using namespace polybar::drawtypes;
TEST(LayoutIconSet, get) {
layouticonset_t layout_icons = make_shared<layouticonset>(make_shared<label>("default-icon"));
EXPECT_EQ("default-icon", layout_icons->get("", "")->get());
EXPECT_EQ("default-icon", layout_icons->get("any_layout", "")->get());
EXPECT_EQ("default-icon", layout_icons->get("", "any_variant")->get());
EXPECT_EQ("default-icon", layout_icons->get("any_layout", "any_variant")->get());
// us;icon => layout 'us' with any variant
EXPECT_TRUE(layout_icons->add("us", layouticonset::VARIANT_ANY, make_shared<label>("us--icon")));
EXPECT_EQ("default-icon", layout_icons->get("", "")->get());
EXPECT_EQ("default-icon", layout_icons->get("any_layout", "")->get());
EXPECT_EQ("default-icon", layout_icons->get("", "any_variant")->get());
EXPECT_EQ("default-icon", layout_icons->get("any_layout", "any_variant")->get());
EXPECT_EQ("default-icon", layout_icons->get("Us", "")->get());
EXPECT_EQ("us--icon", layout_icons->get("us", "")->get());
EXPECT_EQ("us--icon", layout_icons->get("us", "undefined_variant")->get());
// us;colemak;icon => layout 'us' with 'colemak' variant
EXPECT_TRUE(layout_icons->add("us", "colemak", make_shared<label>("us-colemak-icon")));
EXPECT_EQ("us--icon", layout_icons->get("us", "undefined_variant")->get());
EXPECT_EQ("us-colemak-icon", layout_icons->get("us", "colemak")->get());
EXPECT_EQ("us-colemak-icon", layout_icons->get("us", "COLEMAK")->get());
EXPECT_EQ("us-colemak-icon", layout_icons->get("us", "a variant containing CoLeMaK in its description")->get());
// us;;icon => layout 'us' with no variant
EXPECT_TRUE(layout_icons->add("us", "", make_shared<label>("us-no_variant-icon")));
EXPECT_EQ("us-no_variant-icon", layout_icons->get("us", "")->get());
EXPECT_EQ("us--icon", layout_icons->get("us", "undefined_variant")->get());
EXPECT_EQ("us-colemak-icon", layout_icons->get("us", "colemak")->get());
EXPECT_EQ("us-colemak-icon", layout_icons->get("us", "COLEMAK")->get());
EXPECT_EQ("us-colemak-icon", layout_icons->get("us", "a variant containing CoLeMaK in its description")->get());
// _;dvorak;icon => any layout with 'dvorak' variant
EXPECT_TRUE(layout_icons->add(layouticonset::VARIANT_ANY, "dvorak", make_shared<label>("any_layout-dvorak-icon")));
EXPECT_EQ("any_layout-dvorak-icon", layout_icons->get("fr", "dvorak")->get());
EXPECT_EQ("any_layout-dvorak-icon", layout_icons->get("fr", "dVORAk")->get());
EXPECT_EQ("us--icon", layout_icons->get("us", "dvorak")->get());
// us;dvorak;icon => layout 'us' with 'dvorak' variant
EXPECT_TRUE(layout_icons->add("us", "dvorak", make_shared<label>("us-dvorak-icon")));
EXPECT_EQ("any_layout-dvorak-icon", layout_icons->get("fr", "dvorak")->get());
EXPECT_EQ("us-dvorak-icon", layout_icons->get("us", "dvorak")->get());
// _;;icon => any layout with no variant
EXPECT_TRUE(layout_icons->add(layouticonset::VARIANT_ANY, "", make_shared<label>("any_layout-no_variant-icon")));
EXPECT_EQ("any_layout-no_variant-icon", layout_icons->get("fr", "")->get());
EXPECT_EQ("us-no_variant-icon", layout_icons->get("us", "")->get());
EXPECT_TRUE(layout_icons->add("us", "variant2", make_shared<label>("us-variant2-icon")));
EXPECT_EQ("us-colemak-icon", layout_icons->get("us", "a variant containing CoLeMaK & variant2 in its description")->get());
EXPECT_TRUE(layout_icons->add(layouticonset::VARIANT_ANY, "variant2", make_shared<label>("any_layout-variant2-icon")));
EXPECT_EQ("any_layout-dvorak-icon", layout_icons->get("some layout", "a variant containing dvorak & variant2 in its description")->get());
// us;_;icon => layout 'us' with any variant
layouticonset_t layout_icons2 = make_shared<layouticonset>(make_shared<label>("default-icon"));
EXPECT_TRUE(layout_icons2->add("us", "_", make_shared<label>("us-any_variant-icon")));
EXPECT_EQ("us-any_variant-icon", layout_icons2->get("us", "")->get());
EXPECT_EQ("us-any_variant-icon", layout_icons2->get("us", "whatever variant")->get());
EXPECT_FALSE(layout_icons->add(
layouticonset::VARIANT_ANY, layouticonset::VARIANT_ANY, make_shared<label>("any_layout-no_variant-icon")));
}

View File

@ -1,4 +1,5 @@
#include "utils/string.hpp"
#include "common/test.hpp"
using namespace polybar;
@ -20,6 +21,27 @@ TEST(String, compare) {
EXPECT_FALSE(string_util::compare("foo", "bar"));
}
TEST(String, contains) {
EXPECT_TRUE(string_util::contains("fooooobar", "foo"));
EXPECT_TRUE(string_util::contains("barrrrrrfoo", "foo"));
EXPECT_TRUE(string_util::contains("barrfoobazzz", "foo"));
EXPECT_FALSE(string_util::contains("foo", "Foo"));
EXPECT_FALSE(string_util::contains("foo", "bar"));
}
TEST(String, contains_ignore_case) {
EXPECT_TRUE(string_util::contains_ignore_case("fooooobar", "foo"));
EXPECT_TRUE(string_util::contains_ignore_case("barrrrrrfoo", "foo"));
EXPECT_TRUE(string_util::contains_ignore_case("barrfoobazzz", "foo"));
EXPECT_TRUE(string_util::contains_ignore_case("fooooobar", "fOO"));
EXPECT_TRUE(string_util::contains_ignore_case("barrrrrrfoo", "FOo"));
EXPECT_TRUE(string_util::contains_ignore_case("barrfoobazzz", "FoO"));
EXPECT_TRUE(string_util::contains_ignore_case("foo", "Foo"));
EXPECT_FALSE(string_util::contains_ignore_case("foo", "bar"));
EXPECT_TRUE(string_util::contains_ignore_case("foo", ""));
EXPECT_FALSE(string_util::contains_ignore_case("", "bar"));
}
TEST(String, replace) {
EXPECT_EQ("a.c", string_util::replace("abc", "b", "."));
EXPECT_EQ("a.a", string_util::replace("aaa", "a", ".", 1, 2));