diff --git a/CHANGELOG.md b/CHANGELOG.md index fea91a44..0dbf5150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Breaking +- We rewrote our tag parser. This shouldn't break anything, if you experience + any problems, please let us know. + The new parser now gives errors for certain invalid tags where the old parser + would just silently ignore them. Adding extra text to the end of a valid tag + now produces an error. For example, tags like `%{T-a}`, `%{T2abc}`, `%{rfoo}`, + and others will now start producing errors. + This does not affect you unless you are producing your own formatting tags + (for example in a script) and you are using one of these invalid tags. + ### Added - Warn states for the cpu, memory, fs, and battery modules. ([`#570`](https://github.com/polybar/polybar/issues/570), @@ -36,4 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Increased precision for upload and download speeds: 0 decimal places for KB/s (as before), 1 for MB/s and 2 for GB/s. +### Fixed +- Parser error if click command contained `}` + ([`#2040`](https://github.com/polybar/polybar/issues/2040)) + [Unreleased]: https://github.com/polybar/polybar/compare/3.5.2...HEAD diff --git a/include/components/bar.hpp b/include/components/bar.hpp index e8c3aafa..60416d1d 100644 --- a/include/components/bar.hpp +++ b/include/components/bar.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include #include "common.hpp" @@ -9,8 +9,8 @@ #include "errors.hpp" #include "events/signal_fwd.hpp" #include "events/signal_receiver.hpp" -#include "utils/math.hpp" #include "settings.hpp" +#include "utils/math.hpp" #include "x11/types.hpp" #include "x11/window.hpp" @@ -20,11 +20,14 @@ POLYBAR_NS class config; class connection; class logger; -class parser; class renderer; class screen; class taskqueue; class tray_manager; + +namespace tags { + class dispatch; +} // }}} /** @@ -38,7 +41,8 @@ inline double geom_format_to_pixels(std::string str, double max) { if ((i = str.find(':')) != std::string::npos) { std::string a = str.substr(0, i - 1); std::string b = str.substr(i + 1); - return math_util::max(0,math_util::percentage_to_value(strtod(a.c_str(), nullptr), max) + strtod(b.c_str(), nullptr)); + return math_util::max( + 0, math_util::percentage_to_value(strtod(a.c_str(), nullptr), max) + strtod(b.c_str(), nullptr)); } else { if (str.find('%') != std::string::npos) { return math_util::percentage_to_value(strtod(str.c_str(), nullptr), max); @@ -53,15 +57,16 @@ class bar : public xpp::event::sink { + > { public: using make_type = unique_ptr; static make_type make(bool only_initialize_values = false); explicit bar(connection&, signal_emitter&, const config&, const logger&, unique_ptr&&, - unique_ptr&&, unique_ptr&&, unique_ptr&&, bool only_initialize_values); + unique_ptr&&, unique_ptr&&, unique_ptr&&, bool only_initialize_values); ~bar(); const bar_settings settings() const; @@ -108,7 +113,7 @@ class bar : public xpp::event::sink m_screen; unique_ptr m_tray; unique_ptr m_renderer; - unique_ptr m_parser; + unique_ptr m_dispatch; unique_ptr m_taskqueue; bar_settings m_opts{}; diff --git a/include/components/builder.hpp b/include/components/builder.hpp index 8aeeafa3..9ea0ae62 100644 --- a/include/components/builder.hpp +++ b/include/components/builder.hpp @@ -4,6 +4,7 @@ #include "common.hpp" #include "components/types.hpp" +#include "tags/types.hpp" POLYBAR_NS using std::map; @@ -47,7 +48,7 @@ class builder { void overline_close(); void underline(const rgba& color = rgba{}); void underline_close(); - void control(controltag tag); + void control(tags::controltag tag); void action(mousebtn index, string action); void action(mousebtn btn, const modules::module_interface& module, string action, string data); void action(mousebtn index, string action, const label_t& label); @@ -55,19 +56,18 @@ class builder { void action_close(); protected: - - void tag_open(syntaxtag tag, const string& value); - void tag_open(attribute attr); - void tag_close(syntaxtag tag); - void tag_close(attribute attr); + void tag_open(tags::syntaxtag tag, const string& value); + void tag_open(tags::attribute attr); + void tag_close(tags::syntaxtag tag); + void tag_close(tags::attribute attr); private: const bar_settings m_bar; string m_output; - map m_tags{}; - map m_colors{}; - map m_attrs{}; + map m_tags{}; + map m_colors{}; + map m_attrs{}; int m_fontindex{0}; }; diff --git a/include/components/parser.hpp b/include/components/parser.hpp deleted file mode 100644 index c2b9ec90..00000000 --- a/include/components/parser.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include "common.hpp" -#include "errors.hpp" -#include "utils/color.hpp" - -POLYBAR_NS - -class signal_emitter; -enum class attribute; -enum class controltag; -enum class mousebtn; -struct bar_settings; - -DEFINE_ERROR(parser_error); -DEFINE_CHILD_ERROR(unrecognized_token, parser_error); -DEFINE_CHILD_ERROR(unrecognized_attribute, parser_error); -DEFINE_CHILD_ERROR(unclosed_actionblocks, parser_error); - -class parser { - public: - using make_type = unique_ptr; - static make_type make(); - - public: - explicit parser(signal_emitter& emitter); - void parse(const bar_settings& bar, string data); - - protected: - void codeblock(string&& data, const bar_settings& bar); - size_t text(string&& data); - - static rgba parse_color(const string& s, rgba fallback = rgba{0}); - static int parse_fontindex(const string& s); - static attribute parse_attr(const char attr); - mousebtn parse_action_btn(const string& data); - static string parse_action_cmd(string&& data); - static controltag parse_control(const string& data); - - private: - signal_emitter& m_sig; - vector m_actions; - unique_ptr m_parser; -}; - -POLYBAR_NS_END diff --git a/include/components/types.hpp b/include/components/types.hpp index acc7b5bc..42999a7f 100644 --- a/include/components/types.hpp +++ b/include/components/types.hpp @@ -32,34 +32,20 @@ enum class edge { NONE = 0, TOP, BOTTOM, LEFT, RIGHT, ALL }; enum class alignment { NONE = 0, LEFT, CENTER, RIGHT }; -enum class attribute { NONE = 0, UNDERLINE, OVERLINE }; - -enum class syntaxtag { +enum class mousebtn { NONE = 0, - A, // mouse action - B, // background color - F, // foreground color - T, // font index - O, // pixel offset - R, // flip colors - o, // overline color - u, // underline color - P, // Polybar control tag + LEFT, + MIDDLE, + RIGHT, + SCROLL_UP, + SCROLL_DOWN, + DOUBLE_LEFT, + DOUBLE_MIDDLE, + DOUBLE_RIGHT, + // Terminator value, do not use + BTN_COUNT, }; -/** - * Values for polybar control tags - * - * %{P...} tags are tags for internal polybar control commands, they are not - * part of the public interface - */ -enum class controltag { - NONE = 0, - R, // Reset all open tags (B, F, T, o, u). Used at module edges -}; - -enum class mousebtn { NONE = 0, LEFT, MIDDLE, RIGHT, SCROLL_UP, SCROLL_DOWN, DOUBLE_LEFT, DOUBLE_MIDDLE, DOUBLE_RIGHT }; - enum class strut { LEFT = 0, RIGHT, diff --git a/include/events/signal.hpp b/include/events/signal.hpp index dde362b4..45c34f05 100644 --- a/include/events/signal.hpp +++ b/include/events/signal.hpp @@ -2,8 +2,9 @@ #include "common.hpp" #include "components/ipc.hpp" -#include "components/parser.hpp" #include "components/types.hpp" +#include "tags/dispatch.hpp" +#include "tags/types.hpp" #include "utils/functional.hpp" POLYBAR_NS @@ -37,6 +38,7 @@ namespace signals { explicit value_signal(void* data) : m_ptr(data) {} explicit value_signal(ValueType&& data) : m_ptr(&data) {} + explicit value_signal(ValueType& data) : m_ptr(&data) {} virtual ~value_signal() {} @@ -154,13 +156,13 @@ namespace signals { struct offset_pixel : public detail::value_signal { using base_type::base_type; }; - struct attribute_set : public detail::value_signal { + struct attribute_set : public detail::value_signal { using base_type::base_type; }; - struct attribute_unset : public detail::value_signal { + struct attribute_unset : public detail::value_signal { using base_type::base_type; }; - struct attribute_toggle : public detail::value_signal { + struct attribute_toggle : public detail::value_signal { using base_type::base_type; }; struct action_begin : public detail::value_signal { @@ -172,7 +174,7 @@ namespace signals { struct text : public detail::value_signal { using base_type::base_type; }; - struct control : public detail::value_signal { + struct control : public detail::value_signal { using base_type::base_type; }; } // namespace parser diff --git a/include/modules/meta/base.inl b/include/modules/meta/base.inl index c6f0fdb4..ab963fbd 100644 --- a/include/modules/meta/base.inl +++ b/include/modules/meta/base.inl @@ -95,7 +95,7 @@ namespace modules { m_builder->flush(); if (!m_cache.empty()) { // Add a reset tag after the module - m_builder->control(controltag::R); + m_builder->control(tags::controltag::R); m_cache += m_builder->flush(); } m_changed = false; diff --git a/include/tags/dispatch.hpp b/include/tags/dispatch.hpp new file mode 100644 index 00000000..fc0fc7a7 --- /dev/null +++ b/include/tags/dispatch.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "common.hpp" +#include "errors.hpp" + +POLYBAR_NS + +class signal_emitter; + +enum class mousebtn; +struct bar_settings; +class logger; + +namespace tags { + enum class attribute; + enum class controltag; + + /** + * Calls into the tag parser to parse the given formatting string and then + * sends the right signals for each tag. + */ + class dispatch { + public: + using make_type = unique_ptr; + static make_type make(); + + explicit dispatch(signal_emitter& emitter, const logger& logger); + void parse(const bar_settings& bar, string data); + + protected: + void text(string&& data); + void handle_action(mousebtn btn, bool closing, const string&& cmd); + + private: + signal_emitter& m_sig; + vector m_actions; + const logger& m_log; + }; +} // namespace tags + +POLYBAR_NS_END diff --git a/include/tags/parser.hpp b/include/tags/parser.hpp new file mode 100644 index 00000000..8c4488f4 --- /dev/null +++ b/include/tags/parser.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include "common.hpp" +#include "errors.hpp" +#include "tags/types.hpp" + +POLYBAR_NS + +namespace tags { + + static constexpr char EOL = '\0'; + + class error : public application_error { + public: + using application_error::application_error; + + explicit error(const string& msg) : application_error(msg), msg(msg) {} + + /** + * Context string that contains the text region where the parser error + * happened. + */ + void set_context(const string& ctxt) { + msg.append(" (Context: '" + ctxt + "')"); + } + + virtual const char* what() const noexcept { + return msg.c_str(); + } + + private: + string msg; + }; + +#define DEFINE_INVALID_ERROR(class_name, name) \ + class class_name : public error { \ + public: \ + explicit class_name(const string& val) : error("Invalid " name ": '" + val + "'") {} \ + explicit class_name(const string& val, const string& what) \ + : error("Invalid " name ": '" + val + "' (reason: '" + what + "')") {} \ + } + + DEFINE_INVALID_ERROR(color_error, "color"); + DEFINE_INVALID_ERROR(font_error, "font index"); + DEFINE_INVALID_ERROR(control_error, "control tag"); + DEFINE_INVALID_ERROR(offset_error, "offset"); + DEFINE_INVALID_ERROR(btn_error, "button id"); +#undef DEFINE_INVALID_ERROR + + class token_error : public error { + public: + explicit token_error(char token, char expected) : token_error(string{token}, string{expected}) {} + explicit token_error(char token, const string& expected) : token_error(string{token}, expected) {} + explicit token_error(const string& token, const string& expected) + : error("Expected '" + expected + "' but found '" + + (token.size() == 1 && token.at(0) == EOL ? "" : token) + "'") {} + }; + + class unrecognized_tag : public error { + public: + explicit unrecognized_tag(char tag) : error("Unrecognized formatting tag '%{" + string{tag} + "}'") {} + }; + + class unrecognized_attr : public error { + public: + explicit unrecognized_attr(char attr) : error("Unrecognized attribute '" + string{attr} + "'") {} + }; + + /** + * Thrown when we expect the end of a tag (either } or a space in a compound + * tag. + */ + class tag_end_error : public error { + public: + explicit tag_end_error(char token) + : error("Expected the end of a tag ('}' or ' ') but found '" + + (token == EOL ? "" : string{token}) + "'") {} + }; + + /** + * Recursive-descent parser for polybar's formatting tags. + * + * An input string is parsed into a list of elements, each element is either + * a piece of text or a single formatting tag. + * + * The elements can either be retrieved one-by-one with next_element() or all + * at once with parse(). + */ + class parser { + public: + /** + * Resets the parser state and sets the new string to parse + */ + void set(const string&& input); + + /** + * Whether a call to next_element() suceeds. + */ + bool has_next_element(); + + /** + * Parses at least one element (if available) and returns the first parsed + * element. + */ + element next_element(); + + /** + * Parses the remaining string and returns all parsed elements. + */ + format_string parse(); + + protected: + void parse_step(); + + bool has_next() const; + char next(); + char peek() const; + void revert(); + + void consume(char c); + void consume_space(); + + void parse_tag(); + + void parse_single_tag_content(); + + color_value parse_color(); + int parse_fontindex(); + int parse_offset(); + controltag parse_control(); + std::pair parse_action(); + mousebtn parse_action_btn(); + string parse_action_cmd(); + attribute parse_attribute(); + + void push_char(char c); + void push_text(string&& text); + + string get_tag_value(); + + private: + string input; + size_t pos = 0; + + /** + * Buffers elements that have been parsed but not yet returned to the user. + */ + format_string buf{}; + /** + * Index into buf so that we don't have to call vector.erase everytime. + * + * Only buf[buf_pos, buf.end()) contain valid elements + */ + size_t buf_pos; + }; + +} // namespace tags + +POLYBAR_NS_END diff --git a/include/tags/types.hpp b/include/tags/types.hpp new file mode 100644 index 00000000..151ab90b --- /dev/null +++ b/include/tags/types.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include "common.hpp" +#include "components/types.hpp" +#include "utils/color.hpp" + +POLYBAR_NS + +namespace tags { + + enum class attribute { NONE = 0, UNDERLINE, OVERLINE }; + + enum class attr_activation { NONE, ON, OFF, TOGGLE }; + + enum class syntaxtag { + A, // mouse action + B, // background color + F, // foreground color + T, // font index + O, // pixel offset + R, // flip colors + o, // overline color + u, // underline color + P, // Polybar control tag + l, // Left alignment + r, // Right alignment + c, // Center alignment + }; + + /** + * Values for polybar control tags + * + * %{P...} tags are tags for internal polybar control commands, they are not + * part of the public interface + */ + enum class controltag { + NONE = 0, + R, // Reset all open tags (B, F, T, o, u). Used at module edges + }; + + enum class color_type { RESET = 0, COLOR }; + + struct color_value { + /** + * ARGB color. + * + * Only used if type == COLOR + */ + rgba val{}; + color_type type; + }; + + /** + * Stores information about an action + * + * The actual command string is stored in element.data + */ + struct action_value { + /** + * NONE is only allowed for closing tags + */ + mousebtn btn; + bool closing; + }; + + enum class tag_type { ATTR, FORMAT }; + + union tag_subtype { + syntaxtag format; + attr_activation activation; + }; + + struct tag { + tag_type type; + tag_subtype subtype; + union { + /** + * Used for 'F', 'G', 'o', 'u' formatting tags. + */ + color_value color; + /** + * For for 'A' tags + */ + action_value action; + /** + * For for 'T' tags + */ + int font; + /** + * For for 'O' tags + */ + int offset; + /** + * For for 'P' tags + */ + controltag ctrl; + + /** + * For attribute activations ((-|+|!)(o|u)) + */ + attribute attr; + }; + }; + + struct element { + element(){}; + element(const string&& text) : data{std::move(text)}, is_tag{false} {}; + + string data{}; + tag tag_data{}; + bool is_tag{false}; + }; + + using format_string = vector; + +} // namespace tags + +POLYBAR_NS_END diff --git a/include/utils/color.hpp b/include/utils/color.hpp index e48e19e1..21741539 100644 --- a/include/utils/color.hpp +++ b/include/utils/color.hpp @@ -11,10 +11,10 @@ POLYBAR_NS */ class rgba { public: - enum color_type { NONE, ARGB, ALPHA_ONLY }; + enum class type { NONE = 0, ARGB, ALPHA_ONLY }; explicit rgba(); - explicit rgba(uint32_t value, color_type type = ARGB); + explicit rgba(uint32_t value, type type = type::ARGB); explicit rgba(string hex); operator string() const; @@ -22,7 +22,7 @@ class rgba { bool operator==(const rgba& other) const; uint32_t value() const; - color_type type() const; + type type() const; double alpha_d() const; double red_d() const; @@ -56,7 +56,7 @@ class rgba { * * Cannot be const because we have to assign to it in the constructor and initializer lists are not possible. */ - color_type m_type{NONE}; + enum type m_type { type::NONE }; }; namespace color_util { diff --git a/src/components/bar.cpp b/src/components/bar.cpp index 4ce27a40..ed3c4c59 100644 --- a/src/components/bar.cpp +++ b/src/components/bar.cpp @@ -3,7 +3,6 @@ #include #include "components/config.hpp" -#include "components/parser.hpp" #include "components/renderer.hpp" #include "components/screen.hpp" #include "components/taskqueue.hpp" @@ -11,6 +10,7 @@ #include "drawtypes/label.hpp" #include "events/signal.hpp" #include "events/signal_emitter.hpp" +#include "tags/dispatch.hpp" #include "utils/bspwm.hpp" #include "utils/color.hpp" #include "utils/factory.hpp" @@ -47,7 +47,7 @@ bar::make_type bar::make(bool only_initialize_values) { logger::make(), screen::make(), tray_manager::make(), - parser::make(), + tags::dispatch::make(), taskqueue::make(), only_initialize_values); // clang-format on @@ -59,7 +59,7 @@ bar::make_type bar::make(bool only_initialize_values) { * TODO: Break out all tray handling */ bar::bar(connection& conn, signal_emitter& emitter, const config& config, const logger& logger, - unique_ptr&& screen, unique_ptr&& tray_manager, unique_ptr&& parser, + unique_ptr&& screen, unique_ptr&& tray_manager, unique_ptr&& dispatch, unique_ptr&& taskqueue, bool only_initialize_values) : m_connection(conn) , m_sig(emitter) @@ -67,7 +67,7 @@ bar::bar(connection& conn, signal_emitter& emitter, const config& config, const , m_log(logger) , m_screen(forward(screen)) , m_tray(forward(tray_manager)) - , m_parser(forward(parser)) + , m_dispatch(forward(dispatch)) , m_taskqueue(forward(taskqueue)) { string bs{m_conf.section()}; @@ -361,8 +361,8 @@ void bar::parse(string&& data, bool force) { m_renderer->begin(rect); try { - m_parser->parse(settings(), data); - } catch (const parser_error& err) { + m_dispatch->parse(settings(), data); + } catch (const exception& err) { m_log.err("Failed to parse contents (reason: %s)", err.what()); } diff --git a/src/components/builder.cpp b/src/components/builder.cpp index 2ed7648e..954a15ba 100644 --- a/src/components/builder.cpp +++ b/src/components/builder.cpp @@ -9,6 +9,8 @@ #include "utils/time.hpp" POLYBAR_NS +using namespace tags; + builder::builder(const bar_settings& bar) : m_bar(bar) { reset(); } @@ -18,7 +20,6 @@ void builder::reset() { * the map */ m_tags.clear(); - m_tags[syntaxtag::NONE] = 0; m_tags[syntaxtag::A] = 0; m_tags[syntaxtag::B] = 0; m_tags[syntaxtag::F] = 0; @@ -451,8 +452,6 @@ void builder::tag_open(syntaxtag tag, const string& value) { m_tags[tag]++; switch (tag) { - case syntaxtag::NONE: - break; case syntaxtag::A: append("%{A" + value + "}"); break; @@ -480,6 +479,15 @@ void builder::tag_open(syntaxtag tag, const string& value) { case syntaxtag::P: append("%{P" + value + "}"); break; + case syntaxtag::l: + append("%{l}"); + break; + case syntaxtag::c: + append("%{c}"); + break; + case syntaxtag::r: + append("%{r}"); + break; } } @@ -534,10 +542,12 @@ void builder::tag_close(syntaxtag tag) { case syntaxtag::o: append("%{o-}"); break; - case syntaxtag::NONE: case syntaxtag::R: case syntaxtag::P: case syntaxtag::O: + case syntaxtag::l: + case syntaxtag::c: + case syntaxtag::r: break; } } diff --git a/src/components/parser.cpp b/src/components/parser.cpp deleted file mode 100644 index eb5069a9..00000000 --- a/src/components/parser.cpp +++ /dev/null @@ -1,323 +0,0 @@ -#include "components/parser.hpp" - -#include - -#include "components/types.hpp" -#include "events/signal.hpp" -#include "events/signal_emitter.hpp" -#include "settings.hpp" -#include "utils/color.hpp" -#include "utils/factory.hpp" -#include "utils/memory.hpp" -#include "utils/string.hpp" - -POLYBAR_NS - -using namespace signals::parser; - -/** - * Create instance - */ -parser::make_type parser::make() { - return factory_util::unique(signal_emitter::make()); -} - -/** - * Construct parser instance - */ -parser::parser(signal_emitter& emitter) : m_sig(emitter) {} - -/** - * Process input string - */ -void parser::parse(const bar_settings& bar, string data) { - while (!data.empty()) { - size_t pos{string::npos}; - - if (data.compare(0, 2, "%{") == 0 && (pos = data.find('}')) != string::npos) { - codeblock(data.substr(2, pos - 2), bar); - data.erase(0, pos + 1); - } else if ((pos = data.find("%{")) != string::npos) { - data.erase(0, text(data.substr(0, pos))); - } else { - data.erase(0, text(data.substr(0))); - } - } - - if (!m_actions.empty()) { - throw unclosed_actionblocks(to_string(m_actions.size()) + " unclosed action block(s)"); - } -} - -/** - * Process contents within tag blocks, i.e: %{...} - */ -void parser::codeblock(string&& data, const bar_settings& bar) { - size_t pos; - - while (data.length()) { - data = string_util::ltrim(move(data), ' '); - - if (data.empty()) { - break; - } - - char tag{data[0]}; - - /* - * Contains the string from the current position to the next space or - * closing curly bracket (}) - * - * This may be unsuitable for some tags (e.g. action tag) to use - * These MUST set value to the actual string they parsed from the beginning - * of data (or a string with the same length). The length of value is used - * to progress the cursor further. - * - * example: - * - * data = A1:echo "test": ...} - * - * erase(0,1) - * -> data = 1:echo "test": ...} - * - * case 'A', parse_action_cmd - * -> value = echo "test" - * - * Padding value - * -> value = echo "test"0:: - * - * erase(0, value.length()) - * -> data = ...} - * - */ - string value; - - // Remove the tag - data.erase(0, 1); - - if ((pos = data.find_first_of(" }")) != string::npos) { - value = data.substr(0, pos); - } else { - value = data; - } - - switch (tag) { - case 'B': - m_sig.emit(change_background{parse_color(value, bar.background)}); - break; - - case 'F': - m_sig.emit(change_foreground{parse_color(value, bar.foreground)}); - break; - - case 'T': - m_sig.emit(change_font{parse_fontindex(value)}); - break; - - case 'U': - m_sig.emit(change_underline{parse_color(value, bar.underline.color)}); - m_sig.emit(change_overline{parse_color(value, bar.overline.color)}); - break; - - case 'u': - m_sig.emit(change_underline{parse_color(value, bar.underline.color)}); - break; - - case 'o': - m_sig.emit(change_overline{parse_color(value, bar.overline.color)}); - break; - - case 'R': - m_sig.emit(reverse_colors{}); - break; - - case 'O': - m_sig.emit(offset_pixel{static_cast(std::strtol(value.c_str(), nullptr, 10))}); - break; - - case 'l': - m_sig.emit(change_alignment{alignment::LEFT}); - break; - - case 'c': - m_sig.emit(change_alignment{alignment::CENTER}); - break; - - case 'r': - m_sig.emit(change_alignment{alignment::RIGHT}); - break; - - case '+': - m_sig.emit(attribute_set{parse_attr(value[0])}); - break; - - case '-': - m_sig.emit(attribute_unset{parse_attr(value[0])}); - break; - - case '!': - m_sig.emit(attribute_toggle{parse_attr(value[0])}); - break; - - case 'A': { - bool has_btn_id = (data[0] != ':'); - if (isdigit(data[0]) || !has_btn_id) { - value = parse_action_cmd(data.substr(has_btn_id ? 1 : 0)); - mousebtn btn = parse_action_btn(data); - m_actions.push_back(static_cast(btn)); - - // Unescape colons inside command before sending it to the renderer - auto cmd = string_util::replace_all(value, "\\:", ":"); - m_sig.emit(action_begin{action{btn, cmd}}); - - /* - * make sure value has the same length as the inside of the action - * tag which is btn_id + ':' + value + ':' - */ - if (has_btn_id) { - value += "0"; - } - value += "::"; - } else if (!m_actions.empty()) { - m_sig.emit(action_end{parse_action_btn(value)}); - m_actions.pop_back(); - } - break; - } - - // Internal Polybar control tags - case 'P': - m_sig.emit(control{parse_control(value)}); - break; - - default: - throw unrecognized_token("Unrecognized token '" + string{tag} + "'"); - } - - if (!data.empty()) { - // Remove the parsed string from data - data.erase(0, !value.empty() ? value.length() : 1); - } - } -} - -/** - * Process text contents - */ -size_t parser::text(string&& data) { -#ifdef DEBUG_WHITESPACE - string::size_type p; - while ((p = data.find(' ')) != string::npos) { - data.replace(p, 1, "-"s); - } -#endif - - m_sig.emit(signals::parser::text{forward(data)}); - return data.size(); -} - -/** - * Process color hex string and convert it to the correct value - */ -rgba parser::parse_color(const string& s, rgba fallback) { - if (!s.empty() && s[0] != '-') { - rgba ret = rgba{s}; - - if (!ret.has_color() || ret.type() == rgba::ALPHA_ONLY) { - logger::make().warn( - "Invalid color in formatting tag detected: \"%s\", using fallback \"%s\". This is an issue with one of your " - "formatting tags. If it is not, please report this as a bug.", - s, static_cast(fallback)); - return fallback; - } - - return ret; - } - return fallback; -} - -/** - * Process font index and convert it to the correct value - */ -int parser::parse_fontindex(const string& s) { - if (s.empty() || s[0] == '-') { - return 0; - } - - try { - return std::stoul(s, nullptr, 10); - } catch (const std::invalid_argument& err) { - return 0; - } -} - -/** - * Process attribute token and convert it to the correct value - */ -attribute parser::parse_attr(const char attr) { - switch (attr) { - case 'o': - return attribute::OVERLINE; - case 'u': - return attribute::UNDERLINE; - default: - throw unrecognized_token("Unrecognized attribute '" + string{attr} + "'"); - } -} - -/** - * Process action button token and convert it to the correct value - */ -mousebtn parser::parse_action_btn(const string& data) { - if (data[0] == ':') { - return mousebtn::LEFT; - } else if (isdigit(data[0])) { - return static_cast(data[0] - '0'); - } else if (!m_actions.empty()) { - return static_cast(m_actions.back()); - } else { - return mousebtn::NONE; - } -} - -/** - * Process action command string - * - * data is the action cmd surrounded by unescaped colons followed by an - * arbitrary string - * - * Returns everything inside the unescaped colons as is - */ -string parser::parse_action_cmd(string&& data) { - if (data[0] != ':') { - return ""; - } - - size_t end{1}; - while ((end = data.find(':', end)) != string::npos && data[end - 1] == '\\') { - end++; - } - - if (end == string::npos) { - return ""; - } - - return data.substr(1, end - 1); -} - -controltag parser::parse_control(const string& data) { - if (data.length() != 1) { - return controltag::NONE; - } - - switch (data[0]) { - case 'R': - return controltag::R; - break; - default: - return controltag::NONE; - break; - } -} - -POLYBAR_NS_END diff --git a/src/components/renderer.cpp b/src/components/renderer.cpp index 59f7d612..394e644f 100644 --- a/src/components/renderer.cpp +++ b/src/components/renderer.cpp @@ -379,11 +379,12 @@ void renderer::flush(alignment a) { double fsize = std::max(5.0, std::min(std::abs(overflow), 30.0)); m_log.trace("renderer: Drawing falloff (pos=%g, size=%g, overflow=%g)", visible_width - fsize, fsize, overflow); m_context->save(); - *m_context << cairo::translate{(double) m_rect.x, (double) m_rect.y}; + *m_context << cairo::translate{(double)m_rect.x, (double)m_rect.y}; *m_context << cairo::abspos{0.0, 0.0}; *m_context << cairo::rect{x + visible_width - fsize, y, fsize, h}; m_context->clip(true); - *m_context << cairo::linear_gradient{x + visible_width - fsize, y, x + visible_width, y, {rgba{0x00000000}, rgba{0xFF000000}}}; + *m_context << cairo::linear_gradient{ + x + visible_width - fsize, y, x + visible_width, y, {rgba{0x00000000}, rgba{0xFF000000}}}; m_context->paint(0.25); m_context->restore(); } @@ -480,8 +481,7 @@ double renderer::block_x(alignment a) const { * The center block can be moved to the left if the right block is too large */ base_pos = std::min(base_pos, max_pos - block_w(a) / 2.0); - } - else { + } else { base_pos = (min_pos + max_pos) / 2.0; } @@ -594,7 +594,7 @@ void renderer::fill_background() { * Fill overline color */ void renderer::fill_overline(double x, double w) { - if (m_bar.overline.size && m_attr.test(static_cast(attribute::OVERLINE))) { + if (m_bar.overline.size && m_attr.test(static_cast(tags::attribute::OVERLINE))) { m_log.trace_x("renderer: overline(x=%f, w=%f)", x, w); m_context->save(); *m_context << m_comp_ol; @@ -609,7 +609,7 @@ void renderer::fill_overline(double x, double w) { * Fill underline color */ void renderer::fill_underline(double x, double w) { - if (m_bar.underline.size && m_attr.test(static_cast(attribute::UNDERLINE))) { + if (m_bar.underline.size && m_attr.test(static_cast(tags::attribute::UNDERLINE))) { m_log.trace_x("renderer: underline(x=%f, w=%f)", x, w); m_context->save(); *m_context << m_comp_ul; @@ -877,7 +877,7 @@ bool renderer::on(const signals::parser::control& evt) { auto ctrl = evt.cast(); switch (ctrl) { - case controltag::R: + case tags::controltag::R: m_bg = m_bar.background; m_fg = m_bar.foreground; m_ul = m_bar.underline.color; @@ -886,7 +886,7 @@ bool renderer::on(const signals::parser::control& evt) { m_attr.reset(); break; - case controltag::NONE: + case tags::controltag::NONE: break; } diff --git a/src/tags/dispatch.cpp b/src/tags/dispatch.cpp new file mode 100644 index 00000000..82f49ca3 --- /dev/null +++ b/src/tags/dispatch.cpp @@ -0,0 +1,171 @@ +#include "tags/dispatch.hpp" + +#include + +#include "events/signal.hpp" +#include "events/signal_emitter.hpp" +#include "settings.hpp" +#include "tags/parser.hpp" +#include "utils/color.hpp" +#include "utils/factory.hpp" + +POLYBAR_NS + +using namespace signals::parser; + +namespace tags { + static rgba get_color(tags::color_value c, rgba fallback) { + if (c.type == tags::color_type::RESET) { + return fallback; + } else { + return c.val; + } + } + + /** + * Create instance + */ + dispatch::make_type dispatch::make() { + return factory_util::unique(signal_emitter::make(), logger::make()); + } + + /** + * Construct parser instance + */ + dispatch::dispatch(signal_emitter& emitter, const logger& logger) : m_sig(emitter), m_log(logger) {} + + /** + * Process input string + */ + void dispatch::parse(const bar_settings& bar, string data) { + tags::parser p; + p.set(std::move(data)); + + while (p.has_next_element()) { + tags::element el; + try { + el = p.next_element(); + } catch (const tags::error& e) { + m_log.err("Parser error (reason: %s)", e.what()); + continue; + } + + if (el.is_tag) { + switch (el.tag_data.type) { + case tags::tag_type::FORMAT: + switch (el.tag_data.subtype.format) { + case tags::syntaxtag::A: + handle_action(el.tag_data.action.btn, el.tag_data.action.closing, std::move(el.data)); + break; + case tags::syntaxtag::B: + m_sig.emit(change_background{get_color(el.tag_data.color, bar.background)}); + break; + case tags::syntaxtag::F: + m_sig.emit(change_foreground{get_color(el.tag_data.color, bar.foreground)}); + break; + case tags::syntaxtag::T: + m_sig.emit(change_font{el.tag_data.font}); + break; + case tags::syntaxtag::O: + m_sig.emit(offset_pixel{el.tag_data.offset}); + break; + case tags::syntaxtag::R: + m_sig.emit(reverse_colors{}); + break; + case tags::syntaxtag::o: + m_sig.emit(change_overline{get_color(el.tag_data.color, bar.overline.color)}); + break; + case tags::syntaxtag::u: + m_sig.emit(change_underline{get_color(el.tag_data.color, bar.underline.color)}); + break; + case tags::syntaxtag::P: + m_sig.emit(control{el.tag_data.ctrl}); + break; + case tags::syntaxtag::l: + m_sig.emit(change_alignment{alignment::LEFT}); + break; + case tags::syntaxtag::r: + m_sig.emit(change_alignment{alignment::RIGHT}); + break; + case tags::syntaxtag::c: + m_sig.emit(change_alignment{alignment::CENTER}); + break; + default: + throw runtime_error( + "Unrecognized tag format: " + to_string(static_cast(el.tag_data.subtype.format))); + } + break; + case tags::tag_type::ATTR: + tags::attribute act = el.tag_data.attr; + switch (el.tag_data.subtype.activation) { + case tags::attr_activation::ON: + m_sig.emit(attribute_set{act}); + break; + case tags::attr_activation::OFF: + m_sig.emit(attribute_unset{act}); + break; + case tags::attr_activation::TOGGLE: + m_sig.emit(attribute_toggle{act}); + break; + default: + throw runtime_error("Unrecognized attribute activation: " + + to_string(static_cast(el.tag_data.subtype.activation))); + } + break; + } + } else { + text(std::move(el.data)); + } + } + + if (!m_actions.empty()) { + throw runtime_error(to_string(m_actions.size()) + " unclosed action block(s)"); + } + } + + /** + * Process text contents + */ + void dispatch::text(string&& data) { +#ifdef DEBUG_WHITESPACE + string::size_type p; + while ((p = data.find(' ')) != string::npos) { + data.replace(p, 1, "-"s); + } +#endif + + m_sig.emit(signals::parser::text{std::move(data)}); + } + + void dispatch::handle_action(mousebtn btn, bool closing, const string&& cmd) { + if (closing) { + if (btn == mousebtn::NONE) { + if (!m_actions.empty()) { + btn = m_actions.back(); + m_actions.pop_back(); + } else { + m_log.err("parser: Closing action tag without matching tag"); + } + } else { + auto it = std::find(m_actions.crbegin(), m_actions.crend(), btn); + + if (it == m_actions.rend()) { + m_log.err("parser: Closing action tag for button %d without matching opening tag", static_cast(btn)); + } else { + /* + * We can't erase with a reverse iterator, we have to get + * the forward iterator first + * https://stackoverflow.com/a/1830240/5363071 + */ + m_actions.erase(std::next(it).base()); + } + } + m_sig.emit(action_end{btn}); + } else { + m_actions.push_back(btn); + m_sig.emit(action_begin{action{btn, std::move(cmd)}}); + } + } +} // namespace tags + +POLYBAR_NS_END diff --git a/src/tags/parser.cpp b/src/tags/parser.cpp new file mode 100644 index 00000000..0cb39333 --- /dev/null +++ b/src/tags/parser.cpp @@ -0,0 +1,513 @@ +#include "tags/parser.hpp" + +#include +#include + +POLYBAR_NS + +namespace tags { + + bool parser::has_next_element() { + return buf_pos < buf.size() || has_next(); + } + + element parser::next_element() { + if (!has_next_element()) { + throw std::runtime_error("tag parser: No next element"); + } + + if (buf_pos >= buf.size()) { + parse_step(); + } + + if (buf_pos >= buf.size()) { + throw std::runtime_error("tag parser: No next element. THIS IS A BUG. (Context: '" + input + "')"); + } + + element e = buf[buf_pos]; + buf_pos++; + + if (buf_pos == buf.size()) { + buf.clear(); + buf_pos = 0; + } + + return e; + } + + format_string parser::parse() { + format_string parsed; + + while (has_next_element()) { + parsed.push_back(next_element()); + } + + return parsed; + } + + /** + * Performs a single parse step. + * + * This means it will parse text until the next tag is reached or it will + * parse an entire %{...} tag. + */ + void parser::parse_step() { + char c; + + /* + * If we have already parsed text, we can stop if we reach a tag. + */ + bool text_parsed = false; + + size_t start_pos = pos; + + try { + while ((c = next())) { + // TODO here we could think about how to escape an action tag + if (c == '%' && has_next() && peek() == '{') { + /* + * If we have already parsed text, encountering a tag opening means + * we can stop parsing now because we parsed at least one entire + * element (the text up to the beginning of the tag). + */ + if (text_parsed) { + // Put back the '%' + revert(); + break; + } + + consume('{'); + consume_space(); + parse_tag(); + break; + } else { + push_char(c); + text_parsed = true; + } + } + } catch (error& e) { + e.set_context(input.substr(start_pos, pos - start_pos)); + throw; + } + } + + void parser::set(const string&& input) { + this->input = std::move(input); + pos = 0; + buf.clear(); + buf_pos = 0; + } + + bool parser::has_next() const { + return pos < input.size(); + } + + char parser::next() { + char c = peek(); + pos++; + return c; + } + + char parser::peek() const { + if (!has_next()) { + return EOL; + } + + return input[pos]; + } + + /** + * Puts back a single character in the input string. + */ + void parser::revert() { + assert(pos > 0); + pos--; + } + + void parser::consume(char c) { + char n = next(); + if (n != c) { + throw tags::token_error(n, c); + } + } + + void parser::consume_space() { + while (peek() == ' ') { + next(); + } + } + + /** + * Parses an entire %{....} tag. + * + * '%' and '{' were already consumed and we are currently on the first character + * inside the tag. + * At the end of this method, we should be on the closing '}' character (not + * yet consumed). + */ + + void parser::parse_tag() { + if (!has_next()) { + throw token_error(EOL, "Formatting tag content"); + } + + while (has_next()) { + parse_single_tag_content(); + + int p = peek(); + + if (p != ' ' && p != '}') { + throw tag_end_error(p); + } else { + /** + * Consume whitespace between elements inside the tag + */ + consume_space(); + + if (peek() == '}') { + consume('}'); + break; + } + } + } + } + + /** + * Parses a single element inside a formatting tag. + * + * For example it would parse the foreground part of the following tag: + * + * %{F#ff0000 B#ff0000} + * ^ ^ + * | - Pointer at the end + * | + * - Pointer at the start + */ + void parser::parse_single_tag_content() { + char c = next(); + + /** + * %{U...} is a special case because it produces over and underline tags. + */ + if (c == 'U') { + element e{}; + e.is_tag = true; + e.tag_data.type = tag_type::FORMAT; + e.tag_data.subtype.format = syntaxtag::u; + e.tag_data.color = parse_color(); + buf.emplace_back(e); + + e.tag_data.subtype.format = syntaxtag::o; + buf.emplace_back(e); + return; + } + + tag_type type; + tag_subtype sub; + + switch (c) { + // clang-format off + case 'B': sub.format = syntaxtag::B; break; + case 'F': sub.format = syntaxtag::F; break; + case 'u': sub.format = syntaxtag::u; break; + case 'o': sub.format = syntaxtag::o; break; + case 'T': sub.format = syntaxtag::T; break; + case 'R': sub.format = syntaxtag::R; break; + case 'O': sub.format = syntaxtag::O; break; + case 'P': sub.format = syntaxtag::P; break; + case 'A': sub.format = syntaxtag::A; break; + case 'l': sub.format = syntaxtag::l; break; + case 'c': sub.format = syntaxtag::c; break; + case 'r': sub.format = syntaxtag::r; break; + + case '+': sub.activation = attr_activation::ON; break; + case '-': sub.activation = attr_activation::OFF; break; + case '!': sub.activation = attr_activation::TOGGLE; break; + + // clang-format on + + default: + throw unrecognized_tag(c); + } + + switch (c) { + case 'B': + case 'F': + case 'u': + case 'o': + case 'T': + case 'R': + case 'O': + case 'P': + case 'A': + case 'l': + case 'c': + case 'r': + type = tag_type::FORMAT; + break; + + case '+': + case '-': + case '!': + type = tag_type::ATTR; + break; + + default: + throw unrecognized_tag(c); + } + + tag tag_data{}; + tag_data.type = type; + tag_data.subtype = sub; + + element e{}; + e.is_tag = true; + + switch (c) { + case 'B': + case 'F': + case 'u': + case 'o': + tag_data.color = parse_color(); + break; + case 'T': + tag_data.font = parse_fontindex(); + break; + case 'O': + tag_data.offset = parse_offset(); + break; + case 'P': + tag_data.ctrl = parse_control(); + break; + case 'A': + std::tie(tag_data.action, e.data) = parse_action(); + break; + + case '+': + case '-': + case '!': + tag_data.attr = parse_attribute(); + break; + } + + e.tag_data = tag_data; + buf.emplace_back(e); + } + + color_value parser::parse_color() { + string s = get_tag_value(); + + color_value ret; + + if (s.empty() || s == "-") { + ret.type = color_type::RESET; + } else { + rgba c{s}; + + if (!c.has_color()) { + throw color_error(s); + } + + ret.type = color_type::COLOR; + ret.val = c; + } + + return ret; + } + + int parser::parse_fontindex() { + string s = get_tag_value(); + + if (s.empty() || s == "-") { + return 0; + } + + try { + size_t ptr; + int ret = std::stoi(s, &ptr, 10); + + if (ret < 0) { + return 0; + } + + if (ptr != s.size()) { + throw font_error(s, "Font index contains non-number characters"); + } + + return ret; + } catch (const std::exception& err) { + throw font_error(s, err.what()); + } + } + + int parser::parse_offset() { + string s = get_tag_value(); + + if (s.empty()) { + return 0; + } + + try { + size_t ptr; + int ret = std::stoi(s, &ptr, 10); + + if (ptr != s.size()) { + throw offset_error(s, "Offset contains non-number characters"); + } + + return ret; + } catch (const std::exception& err) { + throw offset_error(s, err.what()); + } + return 0; + } + + controltag parser::parse_control() { + string s = get_tag_value(); + + if (s.empty()) { + throw control_error(s, "Control tag is empty"); + } + + switch (s[0]) { + case 'R': + if (s.size() != 1) { + throw control_error(s, "Control tag R has extra data"); + } + + return controltag::R; + default: + throw control_error(s); + } + } + + std::pair parser::parse_action() { + mousebtn btn = parse_action_btn(); + + action_value ret; + + string cmd; + + if (has_next() && peek() == ':') { + ret.btn = btn == mousebtn::NONE ? mousebtn::LEFT : btn; + ret.closing = false; + cmd = parse_action_cmd(); + } else { + ret.btn = btn; + ret.closing = true; + } + + return {ret, cmd}; + } + + /** + * Parses the button index after starting an action tag. + * + * May return mousebtn::NONE if no button was given. + */ + mousebtn parser::parse_action_btn() { + if (has_next()) { + if (isdigit(peek())) { + char c = next(); + int num = c - '0'; + + if (num < static_cast(mousebtn::NONE) || num >= static_cast(mousebtn::BTN_COUNT)) { + throw btn_error(string{c}); + } + + return static_cast(num); + } + } + + return mousebtn::NONE; + } + + /** + * Starts at ':' and parses a complete action string. + * + * Returns the parsed action string with without escaping backslashes. + * + * Afterwards the parsers will be at the character immediately after the + * closing colon. + */ + string parser::parse_action_cmd() { + consume(':'); + + string s; + + char prev = EOL; + + while (has_next()) { + char c = next(); + + if (c == ':') { + if (prev == '\\') { + s.pop_back(); + s.push_back(c); + } else { + break; + } + } else { + s.push_back(c); + } + + prev = c; + } + + return s; + } + + attribute parser::parse_attribute() { + char c; + switch (c = next()) { + case 'u': + return attribute::UNDERLINE; + case 'o': + return attribute::OVERLINE; + default: + throw unrecognized_attr(c); + } + } + + void parser::push_char(char c) { + if (!buf.empty() && buf_pos < buf.size() && !buf.back().is_tag) { + buf.back().data += c; + } else { + buf.emplace_back(string{c}); + } + } + + void parser::push_text(string&& text) { + if (text.empty()) { + return; + } + + if (!buf.empty() && buf_pos < buf.size() && !buf.back().is_tag) { + buf.back().data += text; + } else { + buf.emplace_back(std::move(text)); + } + } + + /** + * Will read up until the end of the tag value. + * + * Afterwards the parser will be at the character directly after the tag + * value. + * + * This function just reads until it encounters a space or a closing curly + * bracket, so it is not useful for tag values that can contain these + * characters (e.g. action tags). + */ + string parser::get_tag_value() { + string s; + + while (has_next() && peek() != ' ' && peek() != '}') { + s.push_back(next()); + } + + return s; + } +} // namespace tags + +POLYBAR_NS_END diff --git a/src/utils/color.cpp b/src/utils/color.cpp index 3123e2e5..b1b92ee1 100644 --- a/src/utils/color.cpp +++ b/src/utils/color.cpp @@ -1,5 +1,7 @@ #include "utils/color.hpp" +#include + POLYBAR_NS /** @@ -24,6 +26,11 @@ static string normalize_hex(string hex) { hex.erase(0, 1); } + // Check that only valid characters are used + if (!std::all_of(hex.cbegin(), hex.cend(), isxdigit)) { + return ""; + } + if (hex.length() == 2) { // We only have an alpha channel return hex; @@ -51,20 +58,20 @@ static string normalize_hex(string hex) { return hex; } -rgba::rgba() : m_value(0), m_type(NONE) {} -rgba::rgba(uint32_t value, color_type type) : m_value(value), m_type(type) {} +rgba::rgba() : m_value(0), m_type(type::NONE) {} +rgba::rgba(uint32_t value, enum type type) : m_value(value), m_type(type) {} rgba::rgba(string hex) { hex = normalize_hex(hex); if (hex.length() == 0) { m_value = 0; - m_type = NONE; + m_type = type::NONE; } else if (hex.length() == 2) { m_value = std::strtoul(hex.c_str(), nullptr, 16) << 24; - m_type = ALPHA_ONLY; + m_type = type::ALPHA_ONLY; } else { m_value = std::strtoul(hex.c_str(), nullptr, 16); - m_type = ARGB; + m_type = type::ARGB; } } @@ -80,11 +87,11 @@ bool rgba::operator==(const rgba& other) const { } switch (m_type) { - case NONE: + case type::NONE: return true; - case ARGB: + case type::ARGB: return m_value == other.m_value; - case ALPHA_ONLY: + case type::ALPHA_ONLY: return alpha_i() == other.alpha_i(); default: return false; @@ -99,7 +106,7 @@ uint32_t rgba::value() const { return this->m_value; } -rgba::color_type rgba::type() const { +enum rgba::type rgba::type() const { return m_type; } @@ -136,7 +143,7 @@ uint8_t rgba::blue_i() const { } bool rgba::has_color() const { - return m_type != NONE; + return m_type != type::NONE; } /** @@ -154,7 +161,7 @@ rgba rgba::apply_alpha_to(rgba other) const { * \returns the new color if this is ALPHA_ONLY or a copy of this otherwise. */ rgba rgba::try_apply_alpha_to(rgba other) const { - if (m_type == ALPHA_ONLY) { + if (m_type == type::ALPHA_ONLY) { return apply_alpha_to(other); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 632afcce..02bfa07f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -50,19 +50,19 @@ endfunction() add_unit_test(utils/actions) add_unit_test(utils/color) add_unit_test(utils/command) -add_unit_test(utils/math unit_tests) -add_unit_test(utils/memory unit_tests) -add_unit_test(utils/scope unit_tests) -add_unit_test(utils/string unit_tests) +add_unit_test(utils/math) +add_unit_test(utils/memory) +add_unit_test(utils/scope) +add_unit_test(utils/string) add_unit_test(utils/file) add_unit_test(utils/process) add_unit_test(components/command_line) add_unit_test(components/bar) -add_unit_test(components/parser) add_unit_test(components/config_parser) add_unit_test(drawtypes/label) add_unit_test(drawtypes/ramp) add_unit_test(drawtypes/iconset) +add_unit_test(tags/parser) # Run make check to build and run all unit tests add_custom_target(check diff --git a/tests/unit_tests/components/parser.cpp b/tests/unit_tests/components/parser.cpp deleted file mode 100644 index 493e688a..00000000 --- a/tests/unit_tests/components/parser.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "common/test.hpp" -#include "events/signal_emitter.hpp" -#include "components/parser.hpp" - -using namespace polybar; - -class TestableParser : public parser { - using parser::parser; - public: using parser::parse_action_cmd; -}; - -class Parser : public ::testing::Test { - protected: - TestableParser m_parser{signal_emitter::make()}; -}; -/** - * The first element of the pair is the expected return text, the second element - * is the input to parse_action_cmd - */ -class ParseActionCmd : - public Parser, - public ::testing::WithParamInterface> {}; - -vector> parse_action_cmd_list = { - {"abc", ":abc:\\abc"}, - {"abc\\:", ":abc\\::\\abc"}, - {"\\:\\:\\:", ":\\:\\:\\::\\abc"}, -}; - -INSTANTIATE_TEST_SUITE_P(Inst, ParseActionCmd, - ::testing::ValuesIn(parse_action_cmd_list)); - -TEST_P(ParseActionCmd, correctness) { - auto input = GetParam().second; - auto result = m_parser.parse_action_cmd(std::move(input)); - EXPECT_EQ(GetParam().first, result); -} diff --git a/tests/unit_tests/tags/parser.cpp b/tests/unit_tests/tags/parser.cpp new file mode 100644 index 00000000..d29435e5 --- /dev/null +++ b/tests/unit_tests/tags/parser.cpp @@ -0,0 +1,460 @@ +#include "tags/parser.hpp" + +#include "common/test.hpp" + +using namespace polybar; +using namespace tags; + +/** + * Helper class to test parsed data. + * + * The expect_* functions will check that the current element corresponds to + * what is expected and then move to the next element. + * + * The assert_* functions are used internally to check certain properties of the + * current element. + */ +class TestableTagParser : public parser { + public: + TestableTagParser() : TestableTagParser(""){}; + + TestableTagParser(const string&& input) { + setup_parser_test(std::move(input)); + } + + void setup_parser_test(const string& input) { + this->set(std::move(input)); + } + + void expect_done() { + EXPECT_FALSE(has_next_element()); + } + + void expect_text(const string&& exp) { + set_current(); + assert_is_tag(false); + EXPECT_EQ(exp, current.data); + } + + void expect_color_reset(syntaxtag t) { + set_current(); + assert_format(t); + EXPECT_EQ(color_type::RESET, current.tag_data.color.type); + } + + void expect_color(syntaxtag t, const string& color) { + set_current(); + assert_format(t); + rgba c{color}; + EXPECT_EQ(color_type::COLOR, current.tag_data.color.type); + EXPECT_EQ(c, current.tag_data.color.val); + } + + void expect_action_closing(mousebtn exp = mousebtn::NONE) { + set_current(); + assert_format(syntaxtag::A); + EXPECT_TRUE(current.tag_data.action.closing); + EXPECT_EQ(exp, current.tag_data.action.btn); + } + + void expect_action(const string& exp, mousebtn btn) { + set_current(); + assert_format(syntaxtag::A); + EXPECT_FALSE(current.tag_data.action.closing); + EXPECT_EQ(btn, current.tag_data.action.btn); + EXPECT_EQ(exp, current.data); + } + + void expect_font_reset() { + expect_font(0); + } + + void expect_font(unsigned exp) { + set_current(); + assert_format(syntaxtag::T); + EXPECT_EQ(exp, current.tag_data.font); + } + + void expect_offset(int exp) { + set_current(); + assert_format(syntaxtag::O); + EXPECT_EQ(exp, current.tag_data.offset); + } + + void expect_ctrl(controltag exp) { + set_current(); + assert_format(syntaxtag::P); + EXPECT_EQ(exp, current.tag_data.ctrl); + } + + void expect_alignment(syntaxtag exp) { + set_current(); + assert_format(exp); + } + + void expect_activation(attr_activation act, attribute attr) { + set_current(); + assert_type(tag_type::ATTR); + EXPECT_EQ(act, current.tag_data.subtype.activation); + EXPECT_EQ(attr, current.tag_data.attr); + } + + void expect_reverse() { + set_current(); + assert_format(syntaxtag::R); + } + + private: + void assert_format(syntaxtag exp) { + assert_type(tag_type::FORMAT); + ASSERT_EQ(exp, current.tag_data.subtype.format); + } + + void assert_type(tag_type exp) { + assert_is_tag(true); + ASSERT_EQ(exp, current.tag_data.type); + } + + void assert_is_tag(bool exp) { + ASSERT_EQ(exp, current.is_tag); + } + + void assert_has() { + if (!has_next_element()) { + throw std::runtime_error("no next element"); + } + } + + void set_current() { + assert_has(); + current = next_element(); + } + + element current; +}; + +class TagParserTest : public ::testing::Test { + protected: + TestableTagParser p; +}; + +TEST_F(TagParserTest, empty) { + p.setup_parser_test(""); + p.expect_done(); +} + +TEST_F(TagParserTest, text) { + p.setup_parser_test("text"); + p.expect_text("text"); + p.expect_done(); +} + +// Single Tag {{{ + +// Parse Single Color {{{ +/** + * + * + * If the color string is empty, this is supposed to be a color reset + */ +using single_color = std::tuple; + +class ParseSingleColorTest : public TagParserTest, public ::testing::WithParamInterface {}; + +vector parse_single_color_list = { + {"%{B-}", syntaxtag::B, ""}, + {"%{F-}", syntaxtag::F, ""}, + {"%{o-}", syntaxtag::o, ""}, + {"%{u-}", syntaxtag::u, ""}, + {"%{B}", syntaxtag::B, ""}, + {"%{F}", syntaxtag::F, ""}, + {"%{o}", syntaxtag::o, ""}, + {"%{u}", syntaxtag::u, ""}, + {"%{B#f0f0f0}", syntaxtag::B, "#f0f0f0"}, + {"%{F#abc}", syntaxtag::F, "#abc"}, + {"%{o#abcd}", syntaxtag::o, "#abcd"}, + {"%{u#FDE}", syntaxtag::u, "#FDE"}, + {"%{ u#FDE}", syntaxtag::u, "#FDE"}, +}; + +INSTANTIATE_TEST_SUITE_P(Inst, ParseSingleColorTest, ::testing::ValuesIn(parse_single_color_list)); + +TEST_P(ParseSingleColorTest, correctness) { + string input; + syntaxtag t; + string color; + std::tie(input, t, color) = GetParam(); + + p.setup_parser_test(std::move(input)); + + if (color.empty()) { + p.expect_color_reset(t); + } else { + p.expect_color(t, color); + } + + p.expect_done(); +} + +// }}} + +// Parse Single Action {{{ + +/** + * If the third element is an empty string, this is a closing tag. + */ +using single_action = std::tuple; + +class ParseSingleActionTest : public TagParserTest, public ::testing::WithParamInterface {}; + +vector parse_single_action_list = { + {"%{A:cmd:}", mousebtn::LEFT, "cmd"}, + {"%{A1:cmd:}", mousebtn::LEFT, "cmd"}, + {"%{A2:cmd:}", mousebtn::MIDDLE, "cmd"}, + {"%{A3:cmd:}", mousebtn::RIGHT, "cmd"}, + {"%{A4:cmd:}", mousebtn::SCROLL_UP, "cmd"}, + {"%{A5:cmd:}", mousebtn::SCROLL_DOWN, "cmd"}, + {"%{A6:cmd:}", mousebtn::DOUBLE_LEFT, "cmd"}, + {"%{A7:cmd:}", mousebtn::DOUBLE_MIDDLE, "cmd"}, + {"%{A8:cmd:}", mousebtn::DOUBLE_RIGHT, "cmd"}, + {"%{A}", mousebtn::NONE, ""}, + {"%{A1}", mousebtn::LEFT, ""}, + {"%{A2}", mousebtn::MIDDLE, ""}, + {"%{A3}", mousebtn::RIGHT, ""}, + {"%{A4}", mousebtn::SCROLL_UP, ""}, + {"%{A5}", mousebtn::SCROLL_DOWN, ""}, + {"%{A6}", mousebtn::DOUBLE_LEFT, ""}, + {"%{A7}", mousebtn::DOUBLE_MIDDLE, ""}, + {"%{A8}", mousebtn::DOUBLE_RIGHT, ""}, + {"%{A1:a\\:b:}", mousebtn::LEFT, "a:b"}, + {"%{A1:\\:\\:\\::}", mousebtn::LEFT, ":::"}, + {"%{A1:#apps.open.0:}", mousebtn::LEFT, "#apps.open.0"}, + // https://github.com/polybar/polybar/issues/2040 + {"%{A1:cmd | awk '{ print $NF }'):}", mousebtn::LEFT, "cmd | awk '{ print $NF }')"}, +}; + +INSTANTIATE_TEST_SUITE_P(Inst, ParseSingleActionTest, ::testing::ValuesIn(parse_single_action_list)); + +TEST_P(ParseSingleActionTest, correctness) { + string input; + mousebtn btn; + string cmd; + std::tie(input, btn, cmd) = GetParam(); + + p.setup_parser_test(std::move(input)); + + if (cmd.empty()) { + p.expect_action_closing(btn); + } else { + p.expect_action(cmd, btn); + } + + p.expect_done(); +} + +// }}} + +// Parse Single Activation {{{ +using single_activation = std::tuple; + +class ParseSingleActivationTest : public TagParserTest, public ::testing::WithParamInterface {}; + +vector parse_single_activation_list = { + {"%{+u}", attribute::UNDERLINE, attr_activation::ON}, + {"%{-u}", attribute::UNDERLINE, attr_activation::OFF}, + {"%{!u}", attribute::UNDERLINE, attr_activation::TOGGLE}, + {"%{+o}", attribute::OVERLINE, attr_activation::ON}, + {"%{-o}", attribute::OVERLINE, attr_activation::OFF}, + {"%{!o}", attribute::OVERLINE, attr_activation::TOGGLE}, +}; + +INSTANTIATE_TEST_SUITE_P(Inst, ParseSingleActivationTest, ::testing::ValuesIn(parse_single_activation_list)); + +TEST_P(ParseSingleActivationTest, correctness) { + string input; + attribute attr; + attr_activation act; + std::tie(input, attr, act) = GetParam(); + p.setup_parser_test(std::move(input)); + p.expect_activation(act, attr); + p.expect_done(); +} + +// }}} + +TEST_F(TagParserTest, reverse) { + p.setup_parser_test("%{R}"); + p.expect_reverse(); + p.expect_done(); +} + +TEST_F(TagParserTest, font) { + p.setup_parser_test("%{T}"); + p.expect_font_reset(); + p.expect_done(); + + p.setup_parser_test("%{T-}"); + p.expect_font_reset(); + p.expect_done(); + + p.setup_parser_test("%{T-123}"); + p.expect_font_reset(); + p.expect_done(); + + p.setup_parser_test("%{T123}"); + p.expect_font(123); + p.expect_done(); +} + +TEST_F(TagParserTest, offset) { + p.setup_parser_test("%{O}"); + p.expect_offset(0); + p.expect_done(); + + p.setup_parser_test("%{O0}"); + p.expect_offset(0); + p.expect_done(); + + p.setup_parser_test("%{O-112}"); + p.expect_offset(-112); + p.expect_done(); + + p.setup_parser_test("%{O123}"); + p.expect_offset(123); + p.expect_done(); +} + +TEST_F(TagParserTest, alignment) { + p.setup_parser_test("%{l}"); + p.expect_alignment(syntaxtag::l); + p.expect_done(); + + p.setup_parser_test("%{c}"); + p.expect_alignment(syntaxtag::c); + p.expect_done(); + + p.setup_parser_test("%{r}"); + p.expect_alignment(syntaxtag::r); + p.expect_done(); +} + +TEST_F(TagParserTest, ctrl) { + p.setup_parser_test("%{PR}"); + p.expect_ctrl(controltag::R); + p.expect_done(); +} + +/** + * Tests the the legacy %{U...} tag first produces %{u...} and then %{o...} + */ +TEST_F(TagParserTest, UnderOverLine) { + p.setup_parser_test("%{U-}"); + p.expect_color_reset(syntaxtag::u); + p.expect_color_reset(syntaxtag::o); + p.expect_done(); + + p.setup_parser_test("%{U#12ab}"); + p.expect_color(syntaxtag::u, "#12ab"); + p.expect_color(syntaxtag::o, "#12ab"); + p.expect_done(); +} + +// }}} + +TEST_F(TagParserTest, compoundTags) { + p.setup_parser_test("%{F- B#ff0000 A:cmd:}"); + p.expect_color_reset(syntaxtag::F); + p.expect_color(syntaxtag::B, "#ff0000"); + p.expect_action("cmd", mousebtn::LEFT); + p.expect_done(); +} + +TEST_F(TagParserTest, combinations) { + p.setup_parser_test("%{r}%{u#4bffdc +u u#4bffdc} 20% abc%{-u u- PR}"); + p.expect_alignment(syntaxtag::r); + p.expect_color(syntaxtag::u, "#4bffdc"); + p.expect_activation(attr_activation::ON, attribute::UNDERLINE); + p.expect_color(syntaxtag::u, "#4bffdc"); + p.expect_text(" 20% abc"); + p.expect_activation(attr_activation::OFF, attribute::UNDERLINE); + p.expect_color_reset(syntaxtag::u); + p.expect_ctrl(controltag::R); + p.expect_done(); +} + +/** + * The type of exception we expect. + * + * Since we can't directly pass typenames, we go through this enum. + */ +enum class exc { ERR, TOKEN, TAG, TAG_END, COLOR, ATTR, FONT, CTRL, OFFSET, BTN }; + +using exception_test = pair; +class ParseErrorTest : public TagParserTest, public ::testing::WithParamInterface {}; + +vector parse_error_test = { + {"%{F-", exc::TAG_END}, + {"%{Q", exc::TAG}, + {"%{", exc::TOKEN}, + {"%{F#xyz}", exc::COLOR}, + {"%{Ffoo}", exc::COLOR}, + {"%{F-abc}", exc::COLOR}, + {"%{+z}", exc::ATTR}, + {"%{T-abc}", exc::FONT}, + {"%{T12a}", exc::FONT}, + {"%{Tabc}", exc::FONT}, + {"%{?u", exc::TAG}, + {"%{PRabc}", exc::CTRL}, + {"%{P}", exc::CTRL}, + {"%{PA}", exc::CTRL}, + {"%{Oabc}", exc::OFFSET}, + {"%{A2:cmd:cmd:}", exc::TAG_END}, + {"%{A9}", exc::BTN}, + {"%{rQ}", exc::TAG_END}, +}; + +INSTANTIATE_TEST_SUITE_P(Inst, ParseErrorTest, ::testing::ValuesIn(parse_error_test)); + +TEST_P(ParseErrorTest, correctness) { + string input; + exc exception; + std::tie(input, exception) = GetParam(); + + p.setup_parser_test(input); + ASSERT_TRUE(p.has_next_element()); + + switch (exception) { + case exc::ERR: + ASSERT_THROW(p.next_element(), tags::error); + break; + case exc::TOKEN: + ASSERT_THROW(p.next_element(), tags::token_error); + break; + case exc::TAG: + ASSERT_THROW(p.next_element(), tags::unrecognized_tag); + break; + case exc::TAG_END: + ASSERT_THROW(p.next_element(), tags::tag_end_error); + break; + case exc::COLOR: + ASSERT_THROW(p.next_element(), tags::color_error); + break; + case exc::ATTR: + ASSERT_THROW(p.next_element(), tags::unrecognized_attr); + break; + case exc::FONT: + ASSERT_THROW(p.next_element(), tags::font_error); + break; + case exc::CTRL: + ASSERT_THROW(p.next_element(), tags::control_error); + break; + case exc::OFFSET: + ASSERT_THROW(p.next_element(), tags::offset_error); + break; + case exc::BTN: + ASSERT_THROW(p.next_element(), tags::btn_error); + break; + default: + FAIL(); + } +} diff --git a/tests/unit_tests/utils/color.cpp b/tests/unit_tests/utils/color.cpp index 29c0e3f8..1699a532 100644 --- a/tests/unit_tests/utils/color.cpp +++ b/tests/unit_tests/utils/color.cpp @@ -9,7 +9,10 @@ TEST(Rgba, constructor) { EXPECT_FALSE(rgba("#f").has_color()); - EXPECT_EQ(rgba::ALPHA_ONLY, rgba{"#12"}.type()); + EXPECT_FALSE(rgba("#-abc").has_color()); + EXPECT_FALSE(rgba("#xyz").has_color()); + + EXPECT_EQ(rgba::type::ALPHA_ONLY, rgba{"#12"}.type()); EXPECT_EQ(0xff000000, rgba{"#ff"}.value()); @@ -49,22 +52,22 @@ TEST(Rgba, string) { } TEST(Rgba, eq) { - rgba v(0x12, rgba::NONE); + rgba v(0x12, rgba::type::NONE); - EXPECT_TRUE(v == rgba(0, rgba::NONE)); - EXPECT_TRUE(v == rgba(0x11, rgba::NONE)); + EXPECT_TRUE(v == rgba(0, rgba::type::NONE)); + EXPECT_TRUE(v == rgba(0x11, rgba::type::NONE)); EXPECT_FALSE(v == rgba{0x123456}); v = rgba{0xCC123456}; EXPECT_TRUE(v == rgba{0xCC123456}); - EXPECT_FALSE(v == rgba(0xCC123456, rgba::NONE)); + EXPECT_FALSE(v == rgba(0xCC123456, rgba::type::NONE)); v = rgba{"#aa"}; - EXPECT_TRUE(v == rgba(0xaa000000, rgba::ALPHA_ONLY)); - EXPECT_FALSE(v == rgba(0xaa000000, rgba::ARGB)); - EXPECT_FALSE(v == rgba(0xab000000, rgba::ALPHA_ONLY)); + EXPECT_TRUE(v == rgba(0xaa000000, rgba::type::ALPHA_ONLY)); + EXPECT_FALSE(v == rgba(0xaa000000, rgba::type::ARGB)); + EXPECT_FALSE(v == rgba(0xab000000, rgba::type::ALPHA_ONLY)); } TEST(Rgba, hasColor) { @@ -80,7 +83,7 @@ TEST(Rgba, hasColor) { EXPECT_TRUE(v.has_color()); - v = rgba(0x1243, rgba::NONE); + v = rgba(0x1243, rgba::type::NONE); EXPECT_FALSE(v.has_color()); } @@ -97,8 +100,8 @@ TEST(Rgba, channel) { } TEST(Rgba, applyAlphaTo) { - rgba v{0xAA000000, rgba::ALPHA_ONLY}; - rgba modified = v.apply_alpha_to(rgba{0xCC123456, rgba::ALPHA_ONLY}); + rgba v{0xAA000000, rgba::type::ALPHA_ONLY}; + rgba modified = v.apply_alpha_to(rgba{0xCC123456}); EXPECT_EQ(0xAA123456, modified.value()); v = rgba{0xCC999999}; @@ -107,8 +110,8 @@ TEST(Rgba, applyAlphaTo) { } TEST(Rgba, tryApplyAlphaTo) { - rgba v{0xAA000000, rgba::ALPHA_ONLY}; - rgba modified = v.try_apply_alpha_to(rgba{0xCC123456, rgba::ALPHA_ONLY}); + rgba v{0xAA000000, rgba::type::ALPHA_ONLY}; + rgba modified = v.try_apply_alpha_to(rgba{0xCC123456}); EXPECT_EQ(0xAA123456, modified.value()); v = rgba{0xCC999999};