yambar/tag.c
Jordan Isaacs 6113f9b94e
particles/icon: init
Introduce a new icon particle. It follows the icon
spec (https://specifications.freedesktop.org/icon-theme-spec/latest/index.html).
Rendering logic is taken from fuzzel (using nanosvg + libpng), while loading
logic is taken from sway. Standard usage is with `use-tag = false` which expands
the provided string template and then loads the string as the icon name. There
are settings to manually override the base paths, themes, etc. The second usage
which is required for tray support is a special icon tag that transfers raw
pixmaps. With `use-tag = true` it first expands the string, and then uses that
output to find an icon pixmap tag. To reduce memory usage, themes are reference
counted so they can be passed down the configuration stack without having to
load them in multiple times.

For programmability, a fallback particle can be specified if no icon/tag is
found `fallback: ...`. And the new icon pixmap tag can be existence checked in
map conditions using `+{tag_name}`.

Future work to be done in follow up diffs:
1. Icon caching. Currently performs an icon lookup on each instantiation & a
   render on each refresh.
2. Theme caching. Changing theme directories results in a new "theme collection"
   being created resulting in the possibility of duplicated theme loading.
2024-07-23 22:58:23 -07:00

820 lines
21 KiB
C

#include "tag.h"
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LOG_MODULE "tag"
#define LOG_ENABLE_DBG 0
#include "icon.h"
#include "log.h"
#include "module.h"
struct private
{
char *name;
union {
struct {
long value;
long min;
long max;
enum tag_realtime_unit realtime_unit;
} value_as_int;
bool value_as_bool;
double value_as_float;
char *value_as_string;
};
};
struct icon_private {
char *name;
struct icon_pixmaps *pixmaps;
};
static const char *
tag_name(const struct tag *tag)
{
const struct private *priv = tag->private;
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)
{
return 0;
}
static enum tag_realtime_unit
no_realtime(const struct tag *tag)
{
return TAG_REALTIME_NONE;
}
static bool
unimpl_refresh_in(const struct tag *tag, long units)
{
return false;
}
static void
destroy_int_and_float(struct tag *tag)
{
struct private *priv = tag->private;
free(priv->name);
free(priv);
free(tag);
}
static void
destroy_string(struct tag *tag)
{
struct private *priv = tag->private;
free(priv->value_as_string);
destroy_int_and_float(tag);
}
static long
int_min(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_int.min;
}
static long
int_max(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_int.max;
}
static enum tag_realtime_unit
int_realtime(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_int.realtime_unit;
}
static const char *
int_as_string(const struct tag *tag)
{
static char as_string[128];
const struct private *priv = tag->private;
snprintf(as_string, sizeof(as_string), "%ld", priv->value_as_int.value);
return as_string;
}
static long
int_as_int(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_int.value;
}
static bool
int_as_bool(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_int.value;
}
static double
int_as_float(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_int.value;
}
static bool
int_refresh_in(const struct tag *tag, long units)
{
const struct private *priv = tag->private;
if (priv->value_as_int.realtime_unit == TAG_REALTIME_NONE)
return false;
if (tag->owner == NULL || tag->owner->refresh_in == NULL)
return false;
assert(priv->value_as_int.realtime_unit == TAG_REALTIME_SECS
|| priv->value_as_int.realtime_unit == TAG_REALTIME_MSECS);
long milli_seconds = units;
if (priv->value_as_int.realtime_unit == TAG_REALTIME_SECS)
milli_seconds *= 1000;
return tag->owner->refresh_in(tag->owner, milli_seconds);
}
static const char *
bool_as_string(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_bool ? "true" : "false";
}
static long
bool_as_int(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_bool;
}
static bool
bool_as_bool(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_bool;
}
static double
bool_as_float(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_bool;
}
static const char *
float_as_string(const struct tag *tag)
{
static char as_string[128];
const struct private *priv = tag->private;
snprintf(as_string, sizeof(as_string), "%.2f", priv->value_as_float);
return as_string;
}
static long
float_as_int(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_float;
}
static bool
float_as_bool(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_float;
}
static double
float_as_float(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_float;
}
static const char *
string_as_string(const struct tag *tag)
{
const struct private *priv = tag->private;
return priv->value_as_string;
}
static long
string_as_int(const struct tag *tag)
{
const struct private *priv = tag->private;
long value;
int matches = sscanf(priv->value_as_string, "%ld", &value);
return matches == 1 ? value : 0;
}
static bool
string_as_bool(const struct tag *tag)
{
const struct private *priv = tag->private;
uint8_t value;
int matches = sscanf(priv->value_as_string, "%hhu", &value);
return matches == 1 ? value : 0;
}
static double
string_as_float(const struct tag *tag)
{
const struct private *priv = tag->private;
double value;
int matches = sscanf(priv->value_as_string, "%lf", &value);
return matches == 1 ? value : 0;
}
struct tag *
tag_new_int(struct module *owner, const char *name, long value)
{
return tag_new_int_range(owner, name, value, value, value);
}
struct tag *
tag_new_int_range(struct module *owner, const char *name, long value, long min, long max)
{
return tag_new_int_realtime(owner, name, value, min, max, TAG_REALTIME_NONE);
}
struct tag *
tag_new_int_realtime(struct module *owner, const char *name, long value, long min, long max,
enum tag_realtime_unit unit)
{
struct private *priv = malloc(sizeof(*priv));
priv->name = strdup(name);
priv->value_as_int.value = value;
priv->value_as_int.min = min;
priv->value_as_int.max = max;
priv->value_as_int.realtime_unit = unit;
struct tag *tag = malloc(sizeof(*tag));
tag->private = priv;
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;
tag->refresh_in = &int_refresh_in;
tag->as_string = &int_as_string;
tag->as_int = &int_as_int;
tag->as_bool = &int_as_bool;
tag->as_float = &int_as_float;
return tag;
}
struct tag *
tag_new_bool(struct module *owner, const char *name, bool value)
{
struct private *priv = malloc(sizeof(*priv));
priv->name = strdup(name);
priv->value_as_bool = value;
struct tag *tag = malloc(sizeof(*tag));
tag->private = priv;
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;
tag->refresh_in = &unimpl_refresh_in;
tag->as_string = &bool_as_string;
tag->as_int = &bool_as_int;
tag->as_bool = &bool_as_bool;
tag->as_float = &bool_as_float;
return tag;
}
struct tag *
tag_new_float(struct module *owner, const char *name, double value)
{
struct private *priv = malloc(sizeof(*priv));
priv->name = strdup(name);
priv->value_as_float = value;
struct tag *tag = malloc(sizeof(*tag));
tag->private = priv;
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;
tag->refresh_in = &unimpl_refresh_in;
tag->as_string = &float_as_string;
tag->as_int = &float_as_int;
tag->as_bool = &float_as_bool;
tag->as_float = &float_as_float;
return tag;
}
struct tag *
tag_new_string(struct module *owner, const char *name, const char *value)
{
struct private *priv = malloc(sizeof(*priv));
priv->name = strdup(name);
priv->value_as_string = value != NULL ? strdup(value) : strdup("");
struct tag *tag = malloc(sizeof(*tag));
tag->private = priv;
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;
tag->refresh_in = &unimpl_refresh_in;
tag->as_string = &string_as_string;
tag->as_int = &string_as_int;
tag->as_bool = &string_as_bool;
tag->as_float = &string_as_float;
return tag;
}
static const char *
icon_tag_name(const struct icon_tag *icon_tag)
{
const struct icon_private *priv = icon_tag->private;
return priv->name;
}
static struct icon_pixmaps *
pixmaps_as_pixmaps(const struct icon_tag *icon_tag)
{
const struct icon_private *priv = icon_tag->private;
return priv->pixmaps;
}
void
pixmap_destroy(struct icon_tag *icon_tag)
{
struct icon_private *priv = icon_tag->private;
icon_pixmaps_dec(priv->pixmaps);
free(priv->name);
free(priv);
free(icon_tag);
}
struct icon_tag *
icon_tag_new_pixmap(struct module *owner, const char *name, struct icon_pixmaps *pixmaps)
{
if (pixmaps == NULL) {
return NULL;
}
struct icon_private *priv = malloc(sizeof(*priv));
priv->name = strdup(name);
priv->pixmaps = icon_pixmaps_inc(pixmaps);
struct icon_tag *icon_tag = malloc(sizeof(*icon_tag));
icon_tag->private = priv;
icon_tag->owner = owner;
icon_tag->name = &icon_tag_name;
icon_tag->pixmaps = &pixmaps_as_pixmaps;
icon_tag->destroy = &pixmap_destroy;
return icon_tag;
}
const struct tag *
tag_for_name(const struct tag_set *set, const char *name)
{
if (set == NULL)
return NULL;
for (size_t i = 0; i < set->count; i++) {
const struct tag *tag = set->tags[i];
if (strcmp(tag->name(tag), name) == 0)
return tag;
}
return NULL;
}
const struct icon_tag *
icon_tag_for_name(const struct tag_set *set, const char *name)
{
if (set == NULL)
return NULL;
for (size_t i = 0; i < set->icon_count; i++) {
const struct icon_tag *tag = set->icon_tags[i];
if (!tag)
continue;
if (strcmp(tag->name(tag), name) == 0)
return tag;
}
return NULL;
}
struct sbuf {
char *s;
size_t size;
size_t len;
};
static void
sbuf_append_at_most(struct sbuf *s1, const char *s2, size_t n)
{
if (s1->len + n >= s1->size) {
size_t required_size = s1->len + n + 1;
s1->size = 2 * required_size;
s1->s = realloc(s1->s, s1->size);
// s1->s[s1->len] = '\0';
}
memcpy(&s1->s[s1->len], s2, n);
s1->len += n;
s1->s[s1->len] = '\0';
}
static void
sbuf_append(struct sbuf *s1, const char *s2)
{
sbuf_append_at_most(s1, s2, strlen(s2));
}
// stores the number in "*value" on success
static bool
is_number(const char *str, int *value)
{
errno = 0;
char *end;
int v = strtol(str, &end, 10);
if (errno != 0 || *end != '\0')
return false;
*value = v;
return true;
}
char *
tags_expand_template(const char *template, const struct tag_set *tags)
{
if (template == NULL)
return NULL;
struct sbuf formatted = {0};
while (true) {
/* Find next tag opening '{' */
const char *begin = strchr(template, '{');
if (begin == NULL) {
/* No more tags, copy remaining characters */
sbuf_append(&formatted, template);
break;
}
/* Find closing '}' */
const char *end = strchr(begin, '}');
if (end == NULL) {
/* Wasn't actually a tag, copy as-is instead */
sbuf_append_at_most(&formatted, template, begin - template + 1);
template = begin + 1;
continue;
}
/* Extract tag name + argument*/
char tag_name_and_arg[end - begin];
strncpy(tag_name_and_arg, begin + 1, end - begin - 1);
tag_name_and_arg[end - begin - 1] = '\0';
static const size_t MAX_TAG_ARGS = 4;
const char *tag_name = NULL;
const char *tag_args[MAX_TAG_ARGS];
memset(tag_args, 0, sizeof(tag_args));
{
char *saveptr;
tag_name = strtok_r(tag_name_and_arg, ":", &saveptr);
for (size_t i = 0; i < MAX_TAG_ARGS; i++) {
const char *arg = strtok_r(NULL, ":", &saveptr);
if (arg == NULL)
break;
tag_args[i] = arg;
}
}
/* Lookup tag */
const struct tag *tag = NULL;
if (tag_name == NULL || (tag = tag_for_name(tags, tag_name)) == NULL) {
/* No such tag, copy as-is instead */
sbuf_append_at_most(&formatted, template, begin - template + 1);
template = begin + 1;
continue;
}
/* Copy characters preceding the tag (name) */
sbuf_append_at_most(&formatted, template, begin - template);
/* Parse arguments */
enum {
FMT_DEFAULT,
FMT_HEX,
FMT_OCT,
FMT_PERCENT,
FMT_KBYTE,
FMT_MBYTE,
FMT_GBYTE,
FMT_KIBYTE,
FMT_MIBYTE,
FMT_GIBYTE,
} format
= FMT_DEFAULT;
enum {
VALUE_VALUE,
VALUE_MIN,
VALUE_MAX,
VALUE_UNIT,
} kind
= VALUE_VALUE;
int digits = 0;
int decimals = 2;
bool zero_pad = false;
char *point = NULL;
for (size_t i = 0; i < MAX_TAG_ARGS; i++) {
if (tag_args[i] == NULL)
break;
else if (strcmp(tag_args[i], "hex") == 0)
format = FMT_HEX;
else if (strcmp(tag_args[i], "oct") == 0)
format = FMT_OCT;
else if (strcmp(tag_args[i], "%") == 0)
format = FMT_PERCENT;
else if (strcmp(tag_args[i], "kb") == 0)
format = FMT_KBYTE;
else if (strcmp(tag_args[i], "mb") == 0)
format = FMT_MBYTE;
else if (strcmp(tag_args[i], "gb") == 0)
format = FMT_GBYTE;
else if (strcmp(tag_args[i], "kib") == 0)
format = FMT_KIBYTE;
else if (strcmp(tag_args[i], "mib") == 0)
format = FMT_MIBYTE;
else if (strcmp(tag_args[i], "gib") == 0)
format = FMT_GIBYTE;
else if (strcmp(tag_args[i], "min") == 0)
kind = VALUE_MIN;
else if (strcmp(tag_args[i], "max") == 0)
kind = VALUE_MAX;
else if (strcmp(tag_args[i], "unit") == 0)
kind = VALUE_UNIT;
else if (is_number(tag_args[i], &digits)) // i.e.: "{tag:3}"
zero_pad = tag_args[i][0] == '0';
else if ((point = strchr(tag_args[i], '.')) != NULL) {
*point = '\0';
const char *digits_str = tag_args[i];
const char *decimals_str = point + 1;
if (digits_str[0] != '\0') { // guards against i.e. "{tag:.3}"
if (!is_number(digits_str, &digits)) {
LOG_WARN("tag `%s`: invalid field width formatter. Ignoring...", tag_name);
}
}
if (decimals_str[0] != '\0') { // guards against i.e. "{tag:3.}"
if (!is_number(decimals_str, &decimals)) {
LOG_WARN("tag `%s`: invalid decimals formatter. Ignoring...", tag_name);
}
}
zero_pad = digits_str[0] == '0';
} else
LOG_WARN("invalid tag formatter: %s", tag_args[i]);
}
/* Copy tag value */
switch (kind) {
case VALUE_VALUE:
switch (format) {
case FMT_DEFAULT: {
switch (tag->type(tag)) {
case TAG_TYPE_FLOAT: {
const char *fmt = zero_pad ? "%0*.*f" : "%*.*f";
char str[24];
snprintf(str, sizeof(str), fmt, digits, decimals, tag->as_float(tag));
sbuf_append(&formatted, str);
break;
}
case TAG_TYPE_INT: {
const char *fmt = zero_pad ? "%0*ld" : "%*ld";
char str[24];
snprintf(str, sizeof(str), fmt, digits, tag->as_int(tag));
sbuf_append(&formatted, str);
break;
}
default:
sbuf_append(&formatted, tag->as_string(tag));
break;
}
break;
}
case FMT_HEX:
case FMT_OCT: {
const char *fmt = format == FMT_HEX ? zero_pad ? "%0*lx" : "%*lx" : zero_pad ? "%0*lo" : "%*lo";
char str[24];
snprintf(str, sizeof(str), fmt, digits, tag->as_int(tag));
sbuf_append(&formatted, str);
break;
}
case FMT_PERCENT: {
const long min = tag->min(tag);
const long max = tag->max(tag);
const long cur = tag->as_int(tag);
const char *fmt = zero_pad ? "%0*lu" : "%*lu";
char str[4];
snprintf(str, sizeof(str), fmt, digits, (cur - min) * 100 / (max - min));
sbuf_append(&formatted, str);
break;
}
case FMT_KBYTE:
case FMT_MBYTE:
case FMT_GBYTE:
case FMT_KIBYTE:
case FMT_MIBYTE:
case FMT_GIBYTE: {
const long divider = format == FMT_KBYTE ? 1000
: format == FMT_MBYTE ? 1000 * 1000
: format == FMT_GBYTE ? 1000 * 1000 * 1000
: format == FMT_KIBYTE ? 1024
: format == FMT_MIBYTE ? 1024 * 1024
: format == FMT_GIBYTE ? 1024 * 1024 * 1024
: 1;
char str[24];
if (tag->type(tag) == TAG_TYPE_FLOAT) {
const char *fmt = zero_pad ? "%0*.*f" : "%*.*f";
snprintf(str, sizeof(str), fmt, digits, decimals, tag->as_float(tag) / (double)divider);
} else {
const char *fmt = zero_pad ? "%0*lu" : "%*lu";
snprintf(str, sizeof(str), fmt, digits, tag->as_int(tag) / divider);
}
sbuf_append(&formatted, str);
break;
}
}
break;
case VALUE_MIN:
case VALUE_MAX: {
const long min = tag->min(tag);
const long max = tag->max(tag);
long value = kind == VALUE_MIN ? min : max;
const char *fmt = NULL;
switch (format) {
case FMT_DEFAULT:
fmt = zero_pad ? "%0*ld" : "%*ld";
break;
case FMT_HEX:
fmt = zero_pad ? "%0*lx" : "%*lx";
break;
case FMT_OCT:
fmt = zero_pad ? "%0*lo" : "%*lo";
break;
case FMT_PERCENT:
value = (value - min) * 100 / (max - min);
fmt = zero_pad ? "%0*lu" : "%*lu";
break;
case FMT_KBYTE:
case FMT_MBYTE:
case FMT_GBYTE:
case FMT_KIBYTE:
case FMT_MIBYTE:
case FMT_GIBYTE: {
const long divider = format == FMT_KBYTE ? 1024
: format == FMT_MBYTE ? 1024 * 1024
: format == FMT_GBYTE ? 1024 * 1024 * 1024
: format == FMT_KIBYTE ? 1000
: format == FMT_MIBYTE ? 1000 * 1000
: format == FMT_GIBYTE ? 1000 * 1000 * 1000
: 1;
value /= divider;
fmt = zero_pad ? "%0*lu" : "%*lu";
break;
}
}
assert(fmt != NULL);
char str[24];
snprintf(str, sizeof(str), fmt, digits, value);
sbuf_append(&formatted, str);
break;
}
case VALUE_UNIT: {
const char *value = NULL;
switch (tag->realtime(tag)) {
case TAG_REALTIME_NONE:
value = "";
break;
case TAG_REALTIME_SECS:
value = "s";
break;
case TAG_REALTIME_MSECS:
value = "ms";
break;
}
sbuf_append(&formatted, value);
break;
}
}
/* Skip past tag name + closing '}' */
template = end + 1;
}
return formatted.s;
}
void
tags_expand_templates(char *expanded[], const char *template[], size_t nmemb, const struct tag_set *tags)
{
for (size_t i = 0; i < nmemb; i++)
expanded[i] = tags_expand_template(template[i], tags);
}
void
tag_set_destroy(struct tag_set *set)
{
for (size_t i = 0; i < set->count; i++)
set->tags[i]->destroy(set->tags[i]);
for (size_t i = 0; i < set->icon_count; i++) {
if (set->icon_tags[i] != NULL) {
set->icon_tags[i]->destroy(set->icon_tags[i]);
}
}
set->tags = NULL;
set->count = 0;
}