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:
parent
a2968127d1
commit
98dffc292a
@ -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
|
||||
|
33
include/drawtypes/layouticonset.hpp
Normal file
33
include/drawtypes/layouticonset.hpp
Normal 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
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
99
src/drawtypes/layouticonset.cpp
Normal file
99
src/drawtypes/layouticonset.cpp
Normal 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
|
@ -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
|
||||
|
@ -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,8 +301,9 @@ 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) {
|
||||
if(kibibytes < 1024 * 1024) {
|
||||
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 {
|
||||
return filesize_gib(kibibytes, precision_gib, locale);
|
||||
|
@ -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)
|
||||
|
76
tests/unit_tests/drawtypes/layouticonset.cpp
Normal file
76
tests/unit_tests/drawtypes/layouticonset.cpp
Normal 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")));
|
||||
}
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user