Permalink
Fetching contributors…
Cannot retrieve contributors at this time
495 lines (441 sloc) 17.3 KB
#include <glib.h>
#include <playerctl/playerctl-player.h>
#include "playerctl/playerctl-formatter.h"
#include "playerctl/playerctl-common.h"
#include <inttypes.h>
#include <assert.h>
#define LENGTH(array) (sizeof array / sizeof array[0])
G_DEFINE_QUARK(playerctl-formatter-error-quark, playerctl_formatter_error);
enum token_type {
TOKEN_PASSTHROUGH,
TOKEN_VARIABLE,
TOKEN_FUNCTION,
};
struct token {
enum token_type type;
gchar *data;
struct token *arg;
};
enum parser_state {
STATE_INSIDE = 0,
STATE_PARAMS_OPEN,
STATE_PARAMS_CLOSED,
STATE_PASSTHROUGH,
};
struct _PlayerctlFormatterPrivate {
GList *tokens;
};
static struct token *token_create(enum token_type type) {
struct token *token = calloc(1, sizeof(struct token));
token->type = type;
return token;
}
static void token_destroy(struct token *token) {
if (token == NULL) {
return;
}
token_destroy(token->arg);
g_free(token->data);
free(token);
}
static void token_list_destroy(GList *tokens) {
if (tokens == NULL) {
return;
}
g_list_free_full(tokens, (GDestroyNotify)token_destroy);
}
static gboolean token_list_contains_key(GList *tokens, const gchar *key) {
GList *t = NULL;
for (t = tokens; t != NULL; t = t->next) {
struct token *token = t->data;
switch (token->type) {
case TOKEN_VARIABLE:
if (g_strcmp0(token->data, key) == 0) {
return TRUE;
}
break;
case TOKEN_FUNCTION:
if (token->arg != NULL && token->arg->type == TOKEN_VARIABLE &&
g_strcmp0(token->arg->data, key) == 0) {
return TRUE;
}
break;
default:
break;
}
}
return FALSE;
}
static GList *tokenize_format(const char *format, GError **error) {
GList *tokens = NULL;
if (format == NULL) {
return NULL;
}
int len = strlen(format);
char buf[1028];
int buf_len = 0;
if (len >= 1028) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"the maximum format string length is 1028");
return NULL;
}
enum parser_state state = STATE_PASSTHROUGH;
for (int i = 0; i < len; ++i) {
if (format[i] == '{' && i < len + 1 && format[i+1] == '{') {
if (state == STATE_INSIDE) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"{{\" (position %d)", i);
token_list_destroy(tokens);
return NULL;
}
if (buf_len != 0) {
struct token *token = token_create(TOKEN_PASSTHROUGH);
buf[buf_len] = '\0';
token->data = g_strdup(buf);
tokens = g_list_append(tokens, token);
}
i += 1;
buf_len = 0;
state = STATE_INSIDE;
} else if (format[i] == '}' && i < len + 1 && format[i+1] == '}' && state != STATE_PASSTHROUGH) {
if (state == STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"}}\" (expected closing parens: \")\" at position %d)", i);
token_list_destroy(tokens);
return NULL;
}
if (state != STATE_PARAMS_CLOSED) {
buf[buf_len] = '\0';
gchar *name = g_strstrip(g_strdup(buf));
if (strlen(name) == 0) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"got empty template expression at position %d", i);
token_list_destroy(tokens);
g_free(name);
return NULL;
}
struct token *token = token_create(TOKEN_VARIABLE);
token->data = name;
tokens = g_list_append(tokens, token);
} else if (buf_len > 0) {
for (int k = 0; k < buf_len; ++k) {
if (buf[k] != ' ') {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"got unexpected input after closing parens at position %d", i - buf_len + k);
token_list_destroy(tokens);
return NULL;
}
}
}
i += 1;
buf_len = 0;
state = STATE_PASSTHROUGH;
} else if (format[i] == '(' && state != STATE_PASSTHROUGH) {
if (state == STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"(\" at position %d", i);
token_list_destroy(tokens);
return NULL;
}
if (state == STATE_PARAMS_CLOSED) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \"(\" at position %d", i);
token_list_destroy(tokens);
return NULL;
}
buf[buf_len] = '\0';
gchar *name = g_strstrip(g_strdup(buf));
if (strlen(name) == 0) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expected a function name to call at position %d", i);
token_list_destroy(tokens);
g_free(name);
return NULL;
}
struct token *token = token_create(TOKEN_FUNCTION);
token->data = name;
tokens = g_list_append(tokens, token);
buf_len = 0;
state = STATE_PARAMS_OPEN;
} else if (format[i] == ')' && state != STATE_PASSTHROUGH) {
if (state != STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected token: \")\" at position %d", i);
token_list_destroy(tokens);
return NULL;
}
buf[buf_len] = '\0';
gchar *name = g_strstrip(g_strdup(buf));
if (strlen(name) == 0) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expected a function parameter at position %d", i);
token_list_destroy(tokens);
g_free(name);
return NULL;
}
struct token *token = token_create(TOKEN_VARIABLE);
token->data = name;
struct token *fn_token = g_list_last(tokens)->data;
assert(fn_token != NULL);
assert(fn_token->type == TOKEN_FUNCTION);
assert(fn_token->arg == NULL);
fn_token->arg = token;
buf_len = 0;
state = STATE_PARAMS_CLOSED;
} else {
buf[buf_len++] = format[i];
}
}
if (state == STATE_INSIDE || state == STATE_PARAMS_CLOSED) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unmatched opener \"{{\" (expected a matching \"}}\" at the end)");
token_list_destroy(tokens);
return NULL;
} else if (state == STATE_PARAMS_OPEN) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unmatched opener \"(\" (expected a matching \")\")");
token_list_destroy(tokens);
return NULL;
}
if (buf_len > 0) {
buf[buf_len] = '\0';
struct token *token = token_create(TOKEN_PASSTHROUGH);
token->data = g_strdup(buf);
tokens = g_list_append(tokens, token);
}
return tokens;
}
static gchar *helperfn_lc(gchar *key, GVariant *value) {
gchar *printed = pctl_print_gvariant(value);
gchar *printed_lc = g_utf8_strdown(printed, -1);
g_free(printed);
return printed_lc;
}
static gchar *helperfn_uc(gchar *key, GVariant *value) {
gchar *printed = pctl_print_gvariant(value);
gchar *printed_uc = g_utf8_strup(printed, -1);
g_free(printed);
return printed_uc;
}
static gchar *helperfn_duration(gchar *key, GVariant *value) {
// mpris durations are represented as int64 in microseconds
if (!g_variant_type_equal(g_variant_get_type(value), G_VARIANT_TYPE_INT64)) {
return NULL;
}
gint64 duration = g_variant_get_int64(value);
gint64 seconds = (duration / 1000000) % 60;
gint64 minutes = (duration / 1000000 / 60) % 60;
gint64 hours = (duration / 1000000 / 60 / 60);
GString *formatted = g_string_new("");
if (hours != 0) {
g_string_append_printf(formatted, "%" PRId64 ":%02" PRId64 ":%02" PRId64, hours, minutes, seconds);
} else {
g_string_append_printf(formatted, "%" PRId64 ":%02" PRId64, minutes, seconds);
}
return g_string_free(formatted, FALSE);
}
static gchar *helperfn_emoji(gchar *key, GVariant *value) {
g_warning("The emoji() helper function is undocumented and experimental and will change in a future release.");
if (g_strcmp0(key, "status") == 0 &&
g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) {
const gchar *status_str = g_variant_get_string(value, NULL);
PlayerctlPlaybackStatus status = 0;
if (pctl_parse_playback_status(status_str, &status)) {
switch (status) {
case PLAYERCTL_PLAYBACK_STATUS_PLAYING:
return g_strdup("▶️");
case PLAYERCTL_PLAYBACK_STATUS_STOPPED:
return g_strdup("⏹️");
case PLAYERCTL_PLAYBACK_STATUS_PAUSED:
return g_strdup("⏸️");
}
}
} else if (g_strcmp0(key, "volume") == 0 &&
g_variant_is_of_type(value, G_VARIANT_TYPE_DOUBLE)) {
const gdouble volume = g_variant_get_double(value);
if (volume < 0.3333) {
return g_strdup("🔈");
} else if (volume < 0.6666) {
return g_strdup("🔉");
} else {
return g_strdup("🔊");
}
}
return pctl_print_gvariant(value);
}
struct template_helper {
const gchar *name;
gchar *(*func)(gchar *key, GVariant *value);
} helpers[] = {
{"lc", &helperfn_lc},
{"uc", &helperfn_uc},
{"duration", &helperfn_duration},
// EXPERIMENTAL
{"emoji", &helperfn_emoji},
};
static gchar *expand_format(GList *tokens, GVariantDict *context, GError **error) {
GString *expanded;
expanded = g_string_new("");
GList *next = tokens;
while (next != NULL) {
struct token *token = next->data;
switch (token->type) {
case TOKEN_PASSTHROUGH:
expanded = g_string_append(expanded, token->data);
break;
case TOKEN_VARIABLE:
{
gchar *name = token->data;
if (g_variant_dict_contains(context, name)) {
GVariant *value = g_variant_dict_lookup_value(context, name, NULL);
if (value != NULL) {
gchar *value_str = pctl_print_gvariant(value);
expanded = g_string_append(expanded, value_str);
g_variant_unref(value);
g_free(value_str);
}
}
break;
}
case TOKEN_FUNCTION:
{
// XXX: functions must have an argument and that argument must be a
// variable (enforced in the tokenization step)
assert(token->arg != NULL);
assert(token->arg->type == TOKEN_VARIABLE);
gboolean found = FALSE;
gchar *fn_name = token->data;
gchar *arg_name = token->arg->data;
for (gsize i = 0; i < LENGTH(helpers); ++i) {
if (g_strcmp0(helpers[i].name, fn_name) == 0) {
GVariant *value = g_variant_dict_lookup_value(context, arg_name, NULL);
if (value != NULL) {
gchar *result = helpers[i].func(arg_name, value);
if (result != NULL) {
expanded = g_string_append(expanded, result);
g_free(result);
}
g_variant_unref(value);
}
found = TRUE;
break;
}
}
if (!found) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unknown template function: %s", fn_name);
token_list_destroy(tokens);
g_string_free(expanded, TRUE);
return NULL;
}
break;
}
}
next = next->next;
}
return g_string_free(expanded, FALSE);
}
static GVariantDict *get_default_template_context(PlayerctlPlayer *player, GVariant *base) {
GVariantDict *context = g_variant_dict_new(base);
if (!g_variant_dict_contains(context, "artist") &&
g_variant_dict_contains(context, "xesam:artist")) {
GVariant *artist = g_variant_dict_lookup_value(context, "xesam:artist", NULL);
g_variant_dict_insert_value(context, "artist", artist);
g_variant_unref(artist);
}
if (!g_variant_dict_contains(context, "album") &&
g_variant_dict_contains(context, "xesam:album")) {
GVariant *album = g_variant_dict_lookup_value(context, "xesam:album", NULL);
g_variant_dict_insert_value(context, "album", album);
g_variant_unref(album);
}
if (!g_variant_dict_contains(context, "title") &&
g_variant_dict_contains(context, "xesam:title")) {
GVariant *title = g_variant_dict_lookup_value(context, "xesam:title", NULL);
g_variant_dict_insert_value(context, "title", title);
g_variant_unref(title);
}
if (!g_variant_dict_contains(context, "playerName")) {
gchar *player_name = NULL;
g_object_get(player, "player-name", &player_name, NULL);
GVariant *player_name_variant = g_variant_new_string(player_name);
g_variant_dict_insert_value(context, "playerName", player_name_variant);
g_free(player_name);
}
if (!g_variant_dict_contains(context, "playerInstance")) {
gchar *instance = NULL;
g_object_get(player, "player-instance", &instance, NULL);
GVariant *player_instance_variant = g_variant_new_string(instance);
g_variant_dict_insert_value(context, "playerInstance", player_instance_variant);
g_free(instance);
}
if (!g_variant_dict_contains(context, "shuffle")) {
gboolean shuffle = FALSE;
g_object_get(player, "shuffle", &shuffle, NULL);
GVariant *shuffle_variant = g_variant_new_boolean(shuffle);
g_variant_dict_insert_value(context, "shuffle", shuffle_variant);
}
if (!g_variant_dict_contains(context, "status")) {
PlayerctlPlaybackStatus status = 0;
g_object_get(player, "playback-status", &status, NULL);
const gchar *status_str = pctl_playback_status_to_string(status);
GVariant *status_variant = g_variant_new_string(status_str);
g_variant_dict_insert_value(context, "status", status_variant);
}
if (!g_variant_dict_contains(context, "loop")) {
PlayerctlLoopStatus status = 0;
g_object_get(player, "loop-status", &status, NULL);
const gchar *status_str = pctl_loop_status_to_string(status);
GVariant *status_variant = g_variant_new_string(status_str);
g_variant_dict_insert_value(context, "loop", status_variant);
}
if (!g_variant_dict_contains(context, "volume")) {
gdouble level = 0.0;
g_object_get(player, "volume", &level, NULL);
GVariant *volume_variant = g_variant_new_double(level);
g_variant_dict_insert_value(context, "volume", volume_variant);
}
if (!g_variant_dict_contains(context, "position")) {
gint64 position = 0;
g_object_get(player, "position", &position, NULL);
GVariant *position_variant = g_variant_new_int64(position);
g_variant_dict_insert_value(context, "position", position_variant);
}
return context;
}
PlayerctlFormatter *playerctl_formatter_new(const gchar *format, GError **error) {
GError *tmp_error = NULL;
GList *tokens = tokenize_format(format, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
return NULL;
}
PlayerctlFormatter *formatter = calloc(1, sizeof(PlayerctlFormatter));
formatter->priv = calloc(1, sizeof(PlayerctlFormatterPrivate));
formatter->priv->tokens = tokens;
return formatter;
}
void playerctl_formatter_destroy(PlayerctlFormatter *formatter) {
if (formatter == NULL) {
return;
}
token_list_destroy(formatter->priv->tokens);
free(formatter->priv);
free(formatter);
}
gboolean playerctl_formatter_contains_key(PlayerctlFormatter *formatter, const gchar *key) {
return token_list_contains_key(formatter->priv->tokens, key);
}
GVariantDict *playerctl_formatter_default_template_context(
PlayerctlFormatter *formatter, PlayerctlPlayer *player, GVariant *base) {
return get_default_template_context(player, base);
}
gchar *playerctl_formatter_expand_format(PlayerctlFormatter *formatter, GVariantDict *context, GError **error) {
GError *tmp_error = NULL;
gchar *expanded = expand_format(formatter->priv->tokens, context, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
return NULL;
}
return expanded;
}