diff --git a/CHANGELOG.md b/CHANGELOG.md index da20255..f3d8642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,46 @@ ### Changed * Minimum required meson version is now 0.58. +* **BREAKING CHANGE**: overhaul of the `map` particle. Instead of + specifying a `tag` and then an array of `values`, you must now + simply use an array of `conditions`, that consist of: + + ` ` + + where `` is one of: + + `== != < <= > >=` + + Note that boolean tags must be used as is: + + `online` + + `~online # use '~' to match for their falsehood` + + As an example, if you previously had something like: + + ``` + map: + tag: State + values: + unrecognized: + ... + ``` + + You would now write it as: + + ``` + map: + conditions: + State == unrecognized: + ... + ``` + + For a more thorough explanation, see the updated map section in the + man page for yambar-particles([#137][137] and [#175][175]). + + [137]: https://codeberg.org/dnkl/yambar/issues/137 + [175]: https://codeberg.org/dnkl/yambar/issues/172 ### Deprecated @@ -48,6 +88,8 @@ ### Security ### Contributors +* Horus + ## 1.8.0 diff --git a/doc/yambar-modules-foreign-toplevel.5.scd b/doc/yambar-modules-foreign-toplevel.5.scd index 6a65ec3..94c4b84 100644 --- a/doc/yambar-modules-foreign-toplevel.5.scd +++ b/doc/yambar-modules-foreign-toplevel.5.scd @@ -67,10 +67,9 @@ bar: - foreign-toplevel: content: map: - tag: activated - values: - false: {empty: {}} - true: + conditions: + ~activated: {empty: {}} + activated: - string: {text: "{app-id}: {title}"} ``` diff --git a/doc/yambar-modules-i3.5.scd b/doc/yambar-modules-i3.5.scd index 9184e46..f82d131 100644 --- a/doc/yambar-modules-i3.5.scd +++ b/doc/yambar-modules-i3.5.scd @@ -102,10 +102,9 @@ bar: content: "": map: - tag: state default: {string: {text: "{name}"}} - values: - focused: {string: {text: "{name}*"}} + conditions: + state == focused: {string: {text: "{name}*"}} current: { string: {text: "{application}: {title}"}} ``` diff --git a/doc/yambar-modules-removables.5.scd b/doc/yambar-modules-removables.5.scd index cbffebe..ee7911f 100644 --- a/doc/yambar-modules-removables.5.scd +++ b/doc/yambar-modules-removables.5.scd @@ -74,13 +74,12 @@ bar: - removables: content: map: - tag: mounted - values: - false: + conditions: + ~mounted: string: on-click: udisksctl mount -b {device} text: "{label}" - true: + mounted: string: on-click: udisksctl unmount -b {device} text: "{label}" diff --git a/doc/yambar-modules-river.5.scd b/doc/yambar-modules-river.5.scd index 96be605..7ba2441 100644 --- a/doc/yambar-modules-river.5.scd +++ b/doc/yambar-modules-river.5.scd @@ -79,10 +79,9 @@ bar: title: {string: { text: "{seat} - {title}" }} content: map: - tag: occupied - values: - false: {empty: {}} - true: + conditions: + ~occupied: {empty: {}} + occupied: string: margin: 5 text: "{id}: {state}" diff --git a/doc/yambar-modules-script.5.scd b/doc/yambar-modules-script.5.scd index c1f31c6..d021bf5 100644 --- a/doc/yambar-modules-script.5.scd +++ b/doc/yambar-modules-script.5.scd @@ -133,10 +133,9 @@ bar: title|string|{{title}} content: map: - tag: status - values: - Paused: {empty: {}} - Playing: + conditions: + status == Paused: {empty: {}} + status == Playing: content: {string: {text: "{artist} - {title}"}} ``` diff --git a/doc/yambar-modules.5.scd b/doc/yambar-modules.5.scd index 266d9b7..ef47f62 100644 --- a/doc/yambar-modules.5.scd +++ b/doc/yambar-modules.5.scd @@ -68,20 +68,17 @@ in red. ``` content: map: - tag: carrier - values: - false: {empty: {}} - true: + conditions: + ~carrier: {empty: {}} + carrier: map: - tag: state default: {string: {text: , font: *awesome, foreground: ffffff66}} - values: - up: + conditions: + state == up: map: - tag: ipv4 default: {string: {text: , font: *awesome}} - values: - "": {string: {text: , font: *awesome, foreground: ffffff66}} + conditions: + ipv4 == "": {string: {text: , font: *awesome, foreground: ffffff66}} ``` ## Use yaml anchors diff --git a/doc/yambar-particles.5.scd b/doc/yambar-particles.5.scd index 312fb99..dba606a 100644 --- a/doc/yambar-particles.5.scd +++ b/doc/yambar-particles.5.scd @@ -208,48 +208,102 @@ content: # MAP This particle maps the values of a specific tag to different -particles. In addition to explicit tag values, you can also specify a +particles based on conditions. A condition takes either the form of: + + + +Or, for boolean tags: + + + +Where is the tag you would like to map, is one of: + +[- == +:- != +:- >= +:- > +:- <= +:- < + +and is the value you would like to compare it to. + +For boolean tags, negation is done with a preceding '~': + +~ + +To match for empty strings, use ' "" ': + + == "" + +In addition to explicit tag values, you can also specify a default/fallback particle. +Note that conditions are evaluated in the order they appear. *If +multiple conditions are true, the first one will be used*. This means +that in a configuration such as: + +``` +tx-bitrate > 1000: +tx-bitrate > 1000000: +``` + +the second condition would never run, since whenever the second +condition is true, the first is also true. The correct way of doing +this would be to invert the order of the conditions: + +``` +tx-bitrate > 1000000: +tx-bitrate > 1000: +``` + + ## CONFIGURATION [[ *Name* :[ *Type* :[ *Req* :[ *Description* -| tag -: string -: yes -: The tag (name of) which values should be mapped -| values +| conditions : associative array : yes -: An associative array of tag values mapped to particles +: An associative array of conditions (see above) mapped to particles | default : particle : no -: Default particle to use, when tag's value does not match any of the - mapped values. +: Default particle to use, none of the conditions are true ## EXAMPLES ``` content: map: - tag: tag_name default: string: text: this is the default particle; the tag's value is now {tag_name} - values: - one_value: + conditions: + tag == one_value: string: text: tag's value is now one_value - another_value: + tag == another_value: string: text: tag's value is now another_value ``` +For a boolean tag: + +``` +content: + map: + conditions: + tag: + string: + text: tag is true + ~tag: + string: + text: tag is false +``` + # RAMP This particle uses a range tag to index into an array of diff --git a/examples/configurations/laptop.conf b/examples/configurations/laptop.conf index 888b19e..573516c 100644 --- a/examples/configurations/laptop.conf +++ b/examples/configurations/laptop.conf @@ -46,73 +46,65 @@ bar: foreground: 000000ff deco: {stack: [background: {color: bc2b3fff}, <<: *std_underline]} - map: &i3_mode - tag: mode default: - string: margin: 5 text: "{mode}" deco: {background: {color: cc421dff}} - empty: {right-margin: 7} - values: - default: {empty: {}} + conditions: + mode == default: {empty: {}} content: "": map: - tag: state - values: - focused: {string: {<<: [*default, *focused]}} - unfocused: {string: {<<: *default}} - invisible: {string: {<<: [*default, *invisible]}} - urgent: {string: {<<: [*default, *urgent]}} + conditions: + state == focused: {string: {<<: [*default, *focused]}} + state == unfocused: {string: {<<: *default}} + state == invisible: {string: {<<: [*default, *invisible]}} + state == urgent: {string: {<<: [*default, *urgent]}} main: map: - tag: state - values: - focused: {string: {<<: [*main, *focused]}} - unfocused: {string: {<<: *main}} - invisible: {string: {<<: [*main, *invisible]}} - urgent: {string: {<<: [*main, *urgent]}} + conditions: + state == focused: {string: {<<: [*main, *focused]}} + state == unfocused: {string: {<<: *main}} + state == invisible: {string: {<<: [*main, *invisible]}} + state == urgent: {string: {<<: [*main, *urgent]}} surfing: map: - tag: state - values: - focused: {string: {<<: [*surfing, *focused]}} - unfocused: {string: {<<: *surfing}} - invisible: {string: {<<: [*surfing, *invisible]}} - urgent: {string: {<<: [*surfing, *urgent]}} + conditions: + state == focused: {string: {<<: [*surfing, *focused]}} + state == unfocused: {string: {<<: *surfing}} + state == invisible: {string: {<<: [*surfing, *invisible]}} + state == urgent: {string: {<<: [*surfing, *urgent]}} misc: map: - tag: state - values: - focused: {string: {<<: [*misc, *focused]}} - unfocused: {string: {<<: *misc}} - invisible: {string: {<<: [*misc, *invisible]}} - urgent: {string: {<<: [*misc, *urgent]}} + conditions: + state == focused: {string: {<<: [*misc, *focused]}} + state == unfocused: {string: {<<: *misc}} + state == invisible: {string: {<<: [*misc, *invisible]}} + state == urgent: {string: {<<: [*misc, *urgent]}} mail: map: - tag: state - values: - focused: {string: {<<: [*mail, *focused]}} - unfocused: {string: {<<: *mail}} - invisible: {string: {<<: [*mail, *invisible]}} - urgent: {string: {<<: [*mail, *urgent]}} + conditions: + state == focused: {string: {<<: [*mail, *focused]}} + state == unfocused: {string: {<<: *mail}} + state == invisible: {string: {<<: [*mail, *invisible]}} + state == urgent: {string: {<<: [*mail, *urgent]}} music: map: - tag: state - values: - focused: {string: {<<: [*music, *focused]}} - unfocused: {string: {<<: *music}} - invisible: {string: {<<: [*music, *invisible]}} - urgent: {string: {<<: [*music, *urgent]}} + conditions: + state == focused: {string: {<<: [*music, *focused]}} + state == unfocused: {string: {<<: *music}} + state == invisible: {string: {<<: [*music, *invisible]}} + state == urgent: {string: {<<: [*music, *urgent]}} - foreign-toplevel: content: map: - tag: activated - values: - false: {empty: {}} - true: + conditions: + ~activated: {empty: {}} + activated: - string: {text: "{app-id}", foreground: ffa0a0ff} - string: {text: ": {title}"} center: @@ -123,32 +115,28 @@ bar: spacing: 0 items: - map: - tag: state - values: - playing: {string: {text: "{artist}"}} - paused: {string: {text: "{artist}", foreground: ffffff66}} + conditions: + state == playing: {string: {text: "{artist}"}} + state == paused: {string: {text: "{artist}", foreground: ffffff66}} - string: {text: " | ", foreground: ffffff66} - map: - tag: state - values: - playing: {string: {text: "{album}"}} - paused: {string: {text: "{album}", foreground: ffffff66}} + conditions: + state == playing: {string: {text: "{album}"}} + state == paused: {string: {text: "{album}", foreground: ffffff66}} - string: {text: " | ", foreground: ffffff66} - map: - tag: state - values: - playing: {string: {text: "{title}", foreground: ffa0a0ff}} - paused: {string: {text: "{title}", foreground: ffffff66}} + conditions: + state == playing: {string: {text: "{title}", foreground: ffa0a0ff}} + state == paused: {string: {text: "{title}", foreground: ffffff66}} content: map: margin: 10 - tag: state - values: - offline: {string: {text: offline, foreground: ff0000ff}} - stopped: {string: {text: stopped}} - paused: {list: *artist_album_title} - playing: {list: *artist_album_title} + conditions: + state == offline: {string: {text: offline, foreground: ff0000ff}} + state == stopped: {string: {text: stopped}} + state == paused: {list: *artist_album_title} + state == playing: {list: *artist_album_title} right: - removables: @@ -158,24 +146,21 @@ bar: spacing: 5 content: map: - tag: mounted - values: - false: + conditions: + ~mounted: map: - tag: optical on-click: udisksctl mount -b {device} - values: - false: [{string: *drive}, {string: {text: "{label}"}}] - true: [{string: *optical}, {string: {text: "{label}"}}] - true: + conditions: + ~optical: [{string: *drive}, {string: {text: "{label}"}}] + optical: [{string: *optical}, {string: {text: "{label}"}}] + mounted: map: - tag: optical on-click: udisksctl unmount -b {device} - values: - false: + conditions: + ~optical: - string: {<<: *drive, deco: *std_underline} - string: {text: "{label}"} - true: + optical: - string: {<<: *optical, deco: *std_underline} - string: {text: "{label}"} - sway-xkb: @@ -187,36 +172,31 @@ bar: name: enp1s0 content: map: - tag: carrier - values: - false: {empty: {}} - true: + conditions: + ~carrier: {empty: {}} + carrier: map: - tag: state default: {string: {text: , font: *awesome, foreground: ffffff66}} - values: - up: + conditions: + state == up: map: - tag: ipv4 default: {string: {text: , font: *awesome}} - values: - "": {string: {text: , font: *awesome, foreground: ffffff66}} + conditions: + ipv4 == "": {string: {text: , font: *awesome, foreground: ffffff66}} - network: name: wlp2s0 content: map: - tag: state default: {string: {text: , font: *awesome, foreground: ffffff66}} - values: - down: {string: {text: , font: *awesome, foreground: ff0000ff}} - up: + conditions: + state == down: {string: {text: , font: *awesome, foreground: ff0000ff}} + state == up: map: - tag: ipv4 default: - string: {text: , font: *awesome} - string: {text: "{ssid}"} - values: - "": + conditions: + ipv4 == "": - string: {text: , font: *awesome, foreground: ffffff66} - string: {text: "{ssid}", foreground: ffffff66} - alsa: @@ -224,16 +204,14 @@ bar: mixer: Master content: map: - tag: online - values: - false: {string: {text: , font: *awesome, foreground: ff0000ff}} - true: + conditions: + ~online: {string: {text: , font: *awesome, foreground: ff0000ff}} + online: map: on-click: /bin/sh -c "amixer -q sset Speaker unmute && amixer -q sset Headphone unmute && amixer -q sset Master toggle" - tag: muted - values: - true: {string: {text: , font: *awesome, foreground: ffffff66}} - false: + conditions: + muted: {string: {text: , font: *awesome, foreground: ffffff66}} + ~muted: ramp: tag: volume items: @@ -266,19 +244,18 @@ bar: - string: {text: "{capacity}% {estimate}"} content: map: - tag: state - values: - unknown: + conditions: + state == unknown: <<: *discharging - discharging: + state == discharging: <<: *discharging - charging: + state == charging: - string: {text: , foreground: 00ff00ff, font: *awesome} - string: {text: "{capacity}% {estimate}"} - full: + state == full: - string: {text: , foreground: 00ff00ff, font: *awesome} - string: {text: "{capacity}% full"} - not charging: + state == not charging: - ramp: tag: capacity items: diff --git a/examples/configurations/river-tags.conf b/examples/configurations/river-tags.conf index 94e5874..462a329 100644 --- a/examples/configurations/river-tags.conf +++ b/examples/configurations/river-tags.conf @@ -16,16 +16,15 @@ bar: - base: &river_base left-margin: 10 right-margin: 13 - tag: id default: {string: {text: , font: *hack}} - values: - 1: {string: {text: ﳐ, font: *hack}} - 2: {string: {text: , font: *hack}} - 3: {string: {text: , font: *hack}} - 4: {string: {text: , font: *hack}} - 5: {string: {text: , font: *hack}} - 10: {string: {text: "scratchpad", font: *hack}} - 11: {string: {text: "work", font: *hack}} + conditions: + id == 1: {string: {text: ﳐ, font: *hack}} + id == 2: {string: {text: , font: *hack}} + id == 3: {string: {text: , font: *hack}} + id == 4: {string: {text: , font: *hack}} + id == 5: {string: {text: , font: *hack}} + id == 10: {string: {text: "scratchpad", font: *hack}} + id == 11: {string: {text: "work", font: *hack}} content: map: @@ -33,28 +32,25 @@ bar: left: sh -c "riverctl set-focused-tags $((1 << ({id} - 1)))" right: sh -c "riverctl toggle-focused-tags $((1 << ({id} -1)))" middle: sh -c "riverctl toggle-view-tags $((1 << ({id} -1)))" - tag: state - values: - urgent: + conditions: + state == urgent: map: <<: *river_base deco: {background: {color: D08770ff}} - focused: + state == focused: map: <<: *river_base deco: *bg_default - visible: + state == visible: map: - tag: occupied - values: - false: {map: {<<: *river_base}} - true: {map: {<<: *river_base, deco: *bg_default}} - unfocused: + conditions: + ~occupied: {map: {<<: *river_base}} + occupied: {map: {<<: *river_base, deco: *bg_default}} + state == unfocused: map: <<: *river_base - invisible: + state == invisible: map: - tag: occupied - values: - false: {empty: {}} - true: {map: {<<: *river_base, deco: {underline: {size: 3, color: ea6962ff}}}} + conditions: + ~occupied: {empty: {}} + occupied: {map: {<<: *river_base, deco: {underline: {size: 3, color: ea6962ff}}}} diff --git a/particles/map.c b/particles/map.c index abf76a6..c748bea 100644 --- a/particles/map.c +++ b/particles/map.c @@ -1,5 +1,6 @@ #include #include +#include #include #define LOG_MODULE "map" @@ -10,13 +11,228 @@ #include "../plugin.h" #include "dynlist.h" +enum map_op{ + MAP_OP_EQ, + MAP_OP_NE, + MAP_OP_LE, + MAP_OP_LT, + MAP_OP_GE, + MAP_OP_GT, + /* The next two are for bool types */ + MAP_OP_SELF, + MAP_OP_NOT, +}; + +struct map_condition { + char *tag; + enum map_op op; + char *value; +}; + +static char * +trim(char *s) +{ + while (*s == ' ') + s++; + + char *end = s + strlen(s) - 1; + while (*end == ' ') { + *end = '\0'; + end--; + } + + return s; +} + +bool +int_condition(const long tag_value, const long cond_value, enum map_op op) +{ + switch (op) { + case MAP_OP_EQ: return tag_value == cond_value; + case MAP_OP_NE: return tag_value != cond_value; + case MAP_OP_LE: return tag_value <= cond_value; + case MAP_OP_LT: return tag_value < cond_value; + case MAP_OP_GE: return tag_value >= cond_value; + case MAP_OP_GT: return tag_value > cond_value; + default: return false; + } +} + +bool +float_condition(const double tag_value, const double cond_value, enum map_op op) +{ + switch (op) { + case MAP_OP_EQ: return tag_value == cond_value; + case MAP_OP_NE: return tag_value != cond_value; + case MAP_OP_LE: return tag_value <= cond_value; + case MAP_OP_LT: return tag_value < cond_value; + case MAP_OP_GE: return tag_value >= cond_value; + case MAP_OP_GT: return tag_value > cond_value; + default: return false; + } +} + +bool +str_condition(const char* tag_value, const char* cond_value, enum map_op op) +{ + switch (op) { + case MAP_OP_EQ: return strcmp(tag_value, cond_value) == 0; + case MAP_OP_NE: return strcmp(tag_value, cond_value) != 0; + case MAP_OP_LE: return strcmp(tag_value, cond_value) <= 0; + case MAP_OP_LT: return strcmp(tag_value, cond_value) < 0; + case MAP_OP_GE: return strcmp(tag_value, cond_value) >= 0; + case MAP_OP_GT: return strcmp(tag_value, cond_value) > 0; + default: return false; + } +} + +bool +eval_map_condition(const struct map_condition* map_cond, const struct tag_set *tags) +{ + const struct tag *tag = tag_for_name(tags, map_cond->tag); + if (tag == NULL) { + LOG_WARN("tag %s not found", map_cond->tag); + return false; + } + + switch (tag->type(tag)) { + case TAG_TYPE_INT: { + errno = 0; + char *end; + const long cond_value = strtol(map_cond->value, &end, 0); + + if (errno == ERANGE) { + LOG_WARN("value %s is too large", map_cond->value); + return false; + } else if (*end != '\0') { + LOG_WARN("failed to parse %s into int", map_cond->value); + return false; + } + + const long tag_value = tag->as_int(tag); + return int_condition(tag_value, cond_value, map_cond->op); + } + case TAG_TYPE_FLOAT: { + errno = 0; + char *end; + const double cond_value = strtod(map_cond->value, &end); + + if (errno == ERANGE) { + LOG_WARN("value %s is too large", map_cond->value); + return false; + } else if (*end != '\0') { + LOG_WARN("failed to parse %s into float", map_cond->value); + return false; + } + + const double tag_value = tag->as_float(tag); + return float_condition(tag_value, cond_value, map_cond->op); + } + case TAG_TYPE_BOOL: + if (map_cond->op == MAP_OP_SELF) + return tag->as_bool(tag); + else if (map_cond->op == MAP_OP_NOT) + return !tag->as_bool(tag); + else { + LOG_WARN("boolean tag '%s' should be used directly", map_cond->tag); + return false; + } + case TAG_TYPE_STRING: { + const char* tag_value = tag->as_string(tag); + return str_condition(tag_value, map_cond->value, map_cond->op); + } + } + return false; +} + +struct map_condition* +map_condition_from_str(const char *str) +{ + struct map_condition *cond = malloc(sizeof(*cond)); + + /* This strdup is just to discard the 'const' qualifier */ + char *str_cpy = strdup(str); + char *op_str = strpbrk(str_cpy, "=!<>~"); + + /* we've already checked things in the verify functions, so these should all work */ + char *tag = str_cpy; + char *value = NULL; + + if (op_str == NULL) { + cond->tag = strdup(trim(tag)); + cond->op = MAP_OP_SELF; + cond->value = NULL; + free(str_cpy); + return cond; + } + + switch (op_str[0]) { + case '=': + cond->op = MAP_OP_EQ; + value = op_str + 2; + break; + case '!': + cond->op = MAP_OP_NE; + value = op_str + 2; + break; + case '<': + if (op_str[1] == '=') { + cond->op = MAP_OP_LE; + value = op_str + 2; + } else { + cond->op = MAP_OP_LT; + value = op_str + 1; + } + break; + case '>': + if (op_str[1] == '=') { + cond->op = MAP_OP_GE; + value = op_str + 2; + } else { + cond->op = MAP_OP_GT; + value = op_str + 1; + } + break; + case '~': + tag = op_str + 1; + cond->op = MAP_OP_NOT; + break; + } + + /* NULL terminate the tag value */ + op_str[0] = '\0'; + + cond->tag = strdup(trim(tag)); + + cond->value = NULL; + if (value != NULL) { + value = trim(value); + const size_t value_len = strlen(value); + if (value[0] == '"' && value[value_len - 1] == '"') { + value[value_len - 1] = '\0'; + ++value; + } + cond->value = strdup(value); + } + + free(str_cpy); + return cond; +} + +void +free_map_condition(struct map_condition* mc) +{ + free(mc->tag); + free(mc->value); + free(mc); +} + struct particle_map { - const char *tag_value; + struct map_condition *condition; struct particle *particle; }; struct private { - char *tag; struct particle *default_particle; struct particle_map *map; size_t count; @@ -92,21 +308,12 @@ static struct exposable * instantiate(const struct particle *particle, const struct tag_set *tags) { const struct private *p = particle->private; - const struct tag *tag = tag_for_name(tags, p->tag); - - if (tag == NULL) { - return p->default_particle != NULL - ? p->default_particle->instantiate(p->default_particle, tags) - : dynlist_exposable_new(NULL, 0, 0, 0); - } - - const char *tag_value = tag->as_string(tag); struct particle *pp = NULL; for (size_t i = 0; i < p->count; i++) { const struct particle_map *e = &p->map[i]; - if (strcmp(e->tag_value, tag_value) != 0) + if (!eval_map_condition(e->condition, tags)) continue; pp = e->particle; @@ -144,28 +351,25 @@ particle_destroy(struct particle *particle) for (size_t i = 0; i < p->count; i++) { struct particle *pp = p->map[i].particle; pp->destroy(pp); - free((char *)p->map[i].tag_value); + free_map_condition(p->map[i].condition); } free(p->map); - free(p->tag); free(p); particle_default_destroy(particle); } static struct particle * -map_new(struct particle *common, const char *tag, - const struct particle_map particle_map[], size_t count, - struct particle *default_particle) +map_new(struct particle *common, const struct particle_map particle_map[], + size_t count, struct particle *default_particle) { struct private *priv = calloc(1, sizeof(*priv)); - priv->tag = strdup(tag); priv->default_particle = default_particle; priv->count = count; priv->map = malloc(count * sizeof(priv->map[0])); for (size_t i = 0; i < count; i++) { - priv->map[i].tag_value = strdup(particle_map[i].tag_value); + priv->map[i].condition = particle_map[i].condition; priv->map[i].particle = particle_map[i].particle; } @@ -176,7 +380,103 @@ map_new(struct particle *common, const char *tag, } static bool -verify_map_values(keychain_t *chain, const struct yml_node *node) +verify_map_condition_syntax(keychain_t *chain, const struct yml_node *node, + const char *line) +{ + /* We need this strdup to discard the 'const' qualifier */ + char *cond = strdup(line); + char *op_str = strpbrk(cond, " =!<>~"); + if (op_str == NULL) { + char *tag = trim(cond); + if (tag[0] == '\0') + goto syntax_fail_missing_tag; + + free(cond); + return true; + } + op_str = trim(op_str); + + char *tag = cond; + char *value = NULL; + + switch (op_str[0]) { + case '=': + if (op_str[1] != '=') + goto syntax_fail_invalid_op; + + value = op_str + 2; + break; + + case '!': + if (op_str[1] != '=') + goto syntax_fail_invalid_op; + + value = op_str + 2; + break; + + case '<': + if (op_str[1] == '=') + value = op_str + 2; + else + value = op_str + 1; + break; + + case '>': + if (op_str[1] == '=') + value = op_str + 2; + else + value = op_str + 1; + break; + + case '~': + tag = op_str + 1; + if (strpbrk(tag, " =!<>~") != NULL) + goto syntax_fail_bad_not; + value = NULL; + break; + default: + goto syntax_fail_invalid_op; + } + + /* NULL terminate the tag value */ + op_str[0] = '\0'; + + tag = trim(tag); + if (tag[0] == '\0') + goto syntax_fail_missing_tag; + + value = value != NULL ? trim(value) : NULL; + + if (value != NULL && value[0] == '\0') + goto syntax_fail_missing_value; + + free(cond); + return true; + +syntax_fail_invalid_op: + LOG_ERR("%s: \"%s\" invalid operator", conf_err_prefix(chain, node), line); + goto err; + +syntax_fail_missing_tag: + LOG_ERR("%s: \"%s\" missing tag", conf_err_prefix(chain, node), line); + goto err; + +syntax_fail_missing_value: + LOG_ERR("%s: \"%s\" missing value", conf_err_prefix(chain, node), line); + goto err; + +syntax_fail_bad_not: + LOG_ERR("%s: \"%s\" '~' cannot be used with other operators", + conf_err_prefix(chain, node), line); + goto err; + +err: + free(cond); + return false; +} + +static bool +verify_map_conditions(keychain_t *chain, const struct yml_node *node) { if (!yml_is_dict(node)) { LOG_ERR( @@ -195,6 +495,9 @@ verify_map_values(keychain_t *chain, const struct yml_node *node) return false; } + if (!verify_map_condition_syntax(chain, it.key, key)) + return false; + if (!conf_verify_particle(chain_push(chain, key), it.value)) return false; @@ -207,11 +510,10 @@ verify_map_values(keychain_t *chain, const struct yml_node *node) static struct particle * from_conf(const struct yml_node *node, struct particle *common) { - const struct yml_node *tag = yml_get_value(node, "tag"); - const struct yml_node *values = yml_get_value(node, "values"); + const struct yml_node *conditions = yml_get_value(node, "conditions"); const struct yml_node *def = yml_get_value(node, "default"); - struct particle_map particle_map[yml_dict_length(values)]; + struct particle_map particle_map[yml_dict_length(conditions)]; struct conf_inherit inherited = { .font = common->font, @@ -220,28 +522,25 @@ from_conf(const struct yml_node *node, struct particle *common) }; size_t idx = 0; - for (struct yml_dict_iter it = yml_dict_iter(values); + for (struct yml_dict_iter it = yml_dict_iter(conditions); it.key != NULL; yml_dict_next(&it), idx++) { - particle_map[idx].tag_value = yml_value_as_string(it.key); + particle_map[idx].condition = map_condition_from_str(yml_value_as_string(it.key)); particle_map[idx].particle = conf_to_particle(it.value, inherited); } struct particle *default_particle = def != NULL ? conf_to_particle(def, inherited) : NULL; - return map_new( - common, yml_value_as_string(tag), particle_map, yml_dict_length(values), - default_particle); + return map_new(common, particle_map, yml_dict_length(conditions), default_particle); } static bool verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"tag", true, &conf_verify_string}, - {"values", true, &verify_map_values}, + {"conditions", true, &verify_map_conditions}, {"default", false, &conf_verify_particle}, PARTICLE_COMMON_ATTRS, }; diff --git a/tag.c b/tag.c index 66b7f02..5c5684b 100644 --- a/tag.c +++ b/tag.c @@ -32,6 +32,30 @@ tag_name(const struct tag *tag) return priv->name; } +static enum tag_type +bool_type(const struct tag *tag) +{ + return TAG_TYPE_BOOL; +} + +static enum tag_type +int_type(const struct tag *tag) +{ + return TAG_TYPE_INT; +} + +static enum tag_type +float_type(const struct tag *tag) +{ + return TAG_TYPE_FLOAT; +} + +static enum tag_type +string_type(const struct tag *tag) +{ + return TAG_TYPE_STRING; +} + static long unimpl_min_max(const struct tag *tag) { @@ -264,6 +288,7 @@ tag_new_int_realtime(struct module *owner, const char *name, long value, tag->owner = owner; tag->destroy = &destroy_int_and_float; tag->name = &tag_name; + tag->type = &int_type; tag->min = &int_min; tag->max = &int_max; tag->realtime = &int_realtime; @@ -287,6 +312,7 @@ tag_new_bool(struct module *owner, const char *name, bool value) tag->owner = owner; tag->destroy = &destroy_int_and_float; tag->name = &tag_name; + tag->type = &bool_type; tag->min = &unimpl_min_max; tag->max = &unimpl_min_max; tag->realtime = &no_realtime; @@ -310,6 +336,7 @@ tag_new_float(struct module *owner, const char *name, double value) tag->owner = owner; tag->destroy = &destroy_int_and_float; tag->name = &tag_name; + tag->type = &float_type; tag->min = &unimpl_min_max; tag->max = &unimpl_min_max; tag->realtime = &no_realtime; @@ -333,6 +360,7 @@ tag_new_string(struct module *owner, const char *name, const char *value) tag->owner = owner; tag->destroy = &destroy_string; tag->name = &tag_name; + tag->type = &string_type; tag->min = &unimpl_min_max; tag->max = &unimpl_min_max; tag->realtime = &no_realtime; diff --git a/tag.h b/tag.h index d6bfe6a..1e9742a 100644 --- a/tag.h +++ b/tag.h @@ -3,6 +3,13 @@ #include #include +enum tag_type { + TAG_TYPE_BOOL, + TAG_TYPE_INT, + TAG_TYPE_FLOAT, + TAG_TYPE_STRING, +}; + enum tag_realtime_unit { TAG_REALTIME_NONE, TAG_REALTIME_SECS, @@ -17,6 +24,7 @@ struct tag { void (*destroy)(struct tag *tag); const char *(*name)(const struct tag *tag); + enum tag_type (*type)(const struct tag *tag); const char *(*as_string)(const struct tag *tag); long (*as_int)(const struct tag *tag); bool (*as_bool)(const struct tag *tag); diff --git a/test/full-conf-good.yml b/test/full-conf-good.yml index 6270487..d89d494 100644 --- a/test/full-conf-good.yml +++ b/test/full-conf-good.yml @@ -62,10 +62,9 @@ bar: - clock: content: map: - tag: date default: {string: {text: default value}} - values: - 1234: {string: {text: specific value}} + conditions: + date == 1234: {string: {text: specific value}} - clock: content: progress-bar: