Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
1077 lines (922 sloc) 35.2 KB
#include "playerctl/playerctl-formatter.h"
#include <assert.h>
#include <glib.h>
#include <inttypes.h>
#include <playerctl/playerctl-player.h>
#include <stdio.h>
#include "playerctl/playerctl-common.h"
#define LENGTH(array) (sizeof array / sizeof array[0])
#define MAX_ARGS 32
#define INFIX_ADD "+"
#define INFIX_SUB "-"
#define INFIX_MUL "*"
#define INFIX_DIV "/"
// clang-format off
G_DEFINE_QUARK(playerctl-formatter-error-quark, playerctl_formatter_error);
// clang-format on
enum token_type {
TOKEN_VARIABLE,
TOKEN_STRING,
TOKEN_FUNCTION,
TOKEN_NUMBER,
};
struct token {
enum token_type type;
gchar *data;
gdouble numeric_data;
GList *args;
};
enum parser_state {
STATE_EXPRESSION = 0,
STATE_IDENTIFIER,
STATE_STRING,
STATE_NUMBER,
};
enum parse_level {
PARSE_FULL = 0,
PARSE_NEXT_IDENT,
PARSE_MULT_DIV,
};
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_list_destroy(GList *tokens);
static void token_destroy(struct token *token) {
if (token == NULL) {
return;
}
token_list_destroy(token->args);
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) {
if (tokens == NULL) {
return FALSE;
}
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:
return token_list_contains_key(token->args, key);
default:
break;
}
}
return FALSE;
}
static gboolean is_identifier_start_char(gchar c) {
return g_ascii_isalpha(c) || c == '_';
}
static gboolean is_identifier_char(gchar c) {
return g_ascii_isalnum(c) || c == '_' || c == ':';
}
static gboolean is_numeric_char(gchar c) {
return g_ascii_isdigit(c) || c == '.';
}
static gchar *infix_to_identifier(gchar infix) {
switch (infix) {
case '+':
return g_strdup(INFIX_ADD);
case '-':
return g_strdup(INFIX_SUB);
case '*':
return g_strdup(INFIX_MUL);
case '/':
return g_strdup(INFIX_DIV);
default:
assert(false && "not reached");
}
}
static struct token *tokenize_expression(const gchar *format, gint pos, gint *end,
enum parse_level level, GError **error) {
GError *tmp_error = NULL;
int len = strlen(format);
char buf[1028];
int buf_len = 0;
struct token *tok = NULL;
enum parser_state state = STATE_EXPRESSION;
if (pos > len - 1) {
g_set_error(error, playerctl_formatter_error_quark(), 1, "unexpected end of expression");
return NULL;
}
for (int i = pos; i < len; ++i) {
switch (state) {
case STATE_EXPRESSION:
if (format[i] == ' ') {
continue;
} else if (format[i] == '(') {
// ordering parens
tok = tokenize_expression(format, i + 1, end, PARSE_FULL, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
return NULL;
}
if (*end > len - 1 || format[*end] != ')') {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expected \")\" (position %d)", *end);
token_destroy(tok);
return NULL;
}
*end += 1;
goto loop_out;
} else if (format[i] == '+' || format[i] == '-') {
// unary + or -
struct token *operand =
tokenize_expression(format, i + 1, end, PARSE_NEXT_IDENT, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
return NULL;
}
tok = token_create(TOKEN_FUNCTION);
tok->data = infix_to_identifier(format[i]);
tok->args = g_list_append(tok->args, operand);
goto loop_out;
} else if (format[i] == '"') {
state = STATE_STRING;
continue;
} else if (is_numeric_char(format[i])) {
state = STATE_NUMBER;
buf[buf_len++] = format[i];
continue;
} else if (!is_identifier_start_char(format[i])) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"unexpected \"%c\", expected expression (position %d)", format[i], i);
return NULL;
} else {
state = STATE_IDENTIFIER;
buf[buf_len++] = format[i];
continue;
}
break;
case STATE_STRING:
if (format[i] == '"') {
tok = token_create(TOKEN_STRING);
buf[buf_len] = '\0';
tok->data = g_strdup(buf);
i++;
while (i < len && format[i] == ' ') {
i++;
}
*end = i;
// printf("string: '%s'\n", tok->data);
goto loop_out;
} else {
buf[buf_len++] = format[i];
}
break;
case STATE_NUMBER:
if (!is_numeric_char(format[i]) || i == len - 2) {
tok = token_create(TOKEN_NUMBER);
buf[buf_len] = '\0';
tok->data = g_strdup(buf);
char *endptr = NULL;
gdouble number = strtod(tok->data, &endptr);
if (endptr == NULL || *endptr != '\0') {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"invalid number: \"%s\" (position %d)", tok->data, i);
token_destroy(tok);
return NULL;
}
tok->numeric_data = number;
while (i < len && format[i] == ' ') {
i++;
}
*end = i;
// printf("number: '%f'\n", tok->numeric_data);
goto loop_out;
} else {
buf[buf_len++] = format[i];
}
break;
case STATE_IDENTIFIER:
if (format[i] == '(') {
tok = token_create(TOKEN_FUNCTION);
buf[buf_len] = '\0';
tok->data = g_strdup(buf);
i += 1;
// printf("function: '%s'\n", tok->data);
int nargs = 0;
while (TRUE) {
tok->args = g_list_append(
tok->args, tokenize_expression(format, i, end, PARSE_FULL, &tmp_error));
nargs++;
if (nargs > MAX_ARGS) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"maximum args of %d exceeded", MAX_ARGS);
token_destroy(tok);
return NULL;
}
if (tmp_error != NULL) {
token_destroy(tok);
g_propagate_error(error, tmp_error);
return NULL;
}
while (*end < len && format[*end] == ' ') {
*end += 1;
}
if (format[*end] == ')') {
*end += 1;
break;
} else if (format[*end] == ',') {
i = *end + 1;
continue;
} else {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expecting \")\" (position %d)", *end);
token_destroy(tok);
return NULL;
}
}
goto loop_out;
} else if (!is_identifier_char(format[i])) {
tok = token_create(TOKEN_VARIABLE);
buf[buf_len] = '\0';
tok->data = g_strdup(buf);
while (i < len && format[i] == ' ') {
i++;
}
*end = i;
// printf("variable: '%s' end='%c'\n", tok->data, format[*end]);
goto loop_out;
} else {
buf[buf_len] = format[i];
++buf_len;
}
break;
}
}
loop_out:
if (tok == NULL) {
g_set_error(error, playerctl_formatter_error_quark(), 1, "unexpected end of expression");
return NULL;
}
while (*end < len && format[*end] == ' ') {
*end += 1;
}
if (level == PARSE_NEXT_IDENT || *end >= len - 1) {
return tok;
}
gchar infix_id = format[*end];
while (infix_id == '*' || infix_id == '/' || infix_id == '+' || infix_id == '-') {
while (infix_id == '*' || infix_id == '/') {
struct token *operand =
tokenize_expression(format, *end + 1, end, PARSE_NEXT_IDENT, &tmp_error);
if (tmp_error != NULL) {
token_destroy(tok);
g_propagate_error(error, tmp_error);
return NULL;
}
struct token *operation = token_create(TOKEN_FUNCTION);
operation->data = infix_to_identifier(infix_id);
operation->args = g_list_append(operation->args, tok);
operation->args = g_list_append(operation->args, operand);
tok = operation;
infix_id = format[*end];
}
if (level == PARSE_MULT_DIV) {
return tok;
}
if (infix_id == '+' || infix_id == '-') {
struct token *operand =
tokenize_expression(format, *end + 1, end, PARSE_MULT_DIV, &tmp_error);
if (tmp_error != NULL) {
token_destroy(tok);
g_propagate_error(error, tmp_error);
return NULL;
}
struct token *operation = token_create(TOKEN_FUNCTION);
operation->data = infix_to_identifier(infix_id);
operation->args = g_list_append(operation->args, tok);
operation->args = g_list_append(operation->args, operand);
tok = operation;
infix_id = format[*end];
}
}
return tok;
}
static GList *tokenize_format(const char *format, GError **error) {
GError *tmp_error = NULL;
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;
}
for (int i = 0; i < len; ++i) {
if (format[i] == '{' && i < len + 1 && format[i + 1] == '{') {
if (buf_len > 0) {
buf[buf_len] = '\0';
buf_len = 0;
struct token *token = token_create(TOKEN_STRING);
token->data = g_strdup(buf);
// printf("passthrough: '%s'\n", token->data);
tokens = g_list_append(tokens, token);
}
i += 2;
int end = 0;
struct token *token = tokenize_expression(format, i, &end, PARSE_FULL, &tmp_error);
if (tmp_error != NULL) {
token_list_destroy(tokens);
g_propagate_error(error, tmp_error);
return NULL;
}
tokens = g_list_append(tokens, token);
i = end;
while (i < len && format[i] == ' ') {
i++;
}
if (i >= len || format[i] != '}' || format[i + 1] != '}') {
token_list_destroy(tokens);
g_set_error(error, playerctl_formatter_error_quark(), 1,
"expecting \"}}\" (position %d)", i);
return NULL;
}
i += 1;
} else {
buf[buf_len++] = format[i];
}
}
if (buf_len > 0) {
buf[buf_len] = '\0';
struct token *token = token_create(TOKEN_STRING);
token->data = g_strdup(buf);
tokens = g_list_append(tokens, token);
}
return tokens;
}
static GVariant *helperfn_lc(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs != 1) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function lc takes exactly one argument (got %d)", nargs);
return NULL;
}
GVariant *value = args[0];
if (value == NULL) {
return g_variant_new("s", "");
}
gchar *printed = pctl_print_gvariant(value);
gchar *printed_lc = g_utf8_strdown(printed, -1);
GVariant *ret = g_variant_new("s", printed_lc);
g_free(printed);
g_free(printed_lc);
return ret;
}
static GVariant *helperfn_uc(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs != 1) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function uc takes exactly one argument (got %d)", nargs);
return NULL;
}
GVariant *value = args[0];
if (value == NULL) {
return g_variant_new("s", "");
}
gchar *printed = pctl_print_gvariant(value);
gchar *printed_uc = g_utf8_strup(printed, -1);
GVariant *ret = g_variant_new("s", printed_uc);
g_free(printed);
g_free(printed_uc);
return ret;
}
static GVariant *helperfn_duration(struct token *token, GVariant **args, int nargs,
GError **error) {
if (nargs != 1) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function uc takes exactly one argument (got %d)", nargs);
return NULL;
}
GVariant *value = args[0];
if (value == NULL) {
return g_variant_new("s", "");
}
// mpris durations are represented as int64 in microseconds
if (!g_variant_type_equal(g_variant_get_type(value), G_VARIANT_TYPE_INT64)) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function position can only be called on int64 values");
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);
}
gchar *formatted_inner = g_string_free(formatted, FALSE);
GVariant *ret = g_variant_new("s", formatted_inner);
g_free(formatted_inner);
return ret;
}
/* Calls g_markup_escape_text to replace the text with appropriately escaped
characters for XML */
static GVariant *helperfn_markup_escape(struct token *token, GVariant **args, int nargs,
GError **error) {
if (nargs != 1) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function markup_escape takes exactly one argument (got %d)", nargs);
return NULL;
}
GVariant *value = args[0];
if (value == NULL) {
return g_variant_new("s", "");
}
gchar *printed = pctl_print_gvariant(value);
gchar *escaped = g_markup_escape_text(printed, -1);
GVariant *ret = g_variant_new("s", escaped);
g_free(escaped);
g_free(printed);
return ret;
}
static GVariant *helperfn_default(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs != 2) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function default takes exactly two arguments (got %d)", nargs);
return NULL;
}
if (args[0] == NULL && args[1] == NULL) {
return NULL;
}
if (args[0] == NULL) {
g_variant_ref(args[1]);
return args[1];
} else {
if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_STRING) &&
strlen(g_variant_get_string(args[0], NULL)) == 0) {
g_variant_ref(args[1]);
return args[1];
}
g_variant_ref(args[0]);
return args[0];
}
}
static GVariant *helperfn_emoji(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs != 1) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function emoji takes exactly one argument (got %d)", nargs);
return NULL;
}
GVariant *value = args[0];
if (value == NULL) {
return g_variant_new("s", "");
}
struct token *arg_token = g_list_first(token->args)->data;
if (arg_token->type != TOKEN_VARIABLE) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"the emoji function can only be called with a variable");
return NULL;
}
gchar *key = arg_token->data;
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_variant_new("s", "▶️");
case PLAYERCTL_PLAYBACK_STATUS_STOPPED:
return g_variant_new("s", "⏹️");
case PLAYERCTL_PLAYBACK_STATUS_PAUSED:
return g_variant_new("s", "⏸️");
}
}
} 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_variant_new("s", "🔈");
} else if (volume < 0.6666) {
return g_variant_new("s", "🔉");
} else {
return g_variant_new("s", "🔊");
}
}
g_variant_ref(value);
return value;
}
static GVariant *helperfn_trunc(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs != 2) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function trunc takes exactly two arguments (got %d)", nargs);
return NULL;
}
GVariant *value = args[0];
GVariant *len = args[1];
if (value == NULL || len == NULL) {
return g_variant_new("s", "");
}
if (!g_variant_type_equal(g_variant_get_type(len), G_VARIANT_TYPE_DOUBLE)) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"function trunc's length parameter can only be called with an int");
return NULL;
}
gchar *orig = pctl_print_gvariant(value);
gchar *trunc = g_utf8_substring(orig, 0, g_variant_get_double(len));
GString *formatted = g_string_new(trunc);
if (g_utf8_strlen(trunc, 256) < g_utf8_strlen(orig, 256)) {
g_string_append(formatted, "");
}
gchar *formatted_inner = g_string_free(formatted, FALSE);
GVariant *ret = g_variant_new("s", formatted_inner);
g_free(formatted_inner);
g_free(trunc);
g_free(orig);
return ret;
}
static gboolean is_valid_numeric_type(GVariant *value) {
// This is all the types we know about for numeric operations. May be
// expanded at a later time.
if (value == NULL) {
return FALSE;
}
if (g_variant_is_of_type(value, G_VARIANT_TYPE_INT64)) {
return TRUE;
} else if (g_variant_is_of_type(value, G_VARIANT_TYPE_DOUBLE)) {
return TRUE;
}
return FALSE;
}
static gdouble get_double_value(GVariant *value) {
if (g_variant_is_of_type(value, G_VARIANT_TYPE_INT64)) {
return (gdouble)g_variant_get_int64(value);
} else if (g_variant_is_of_type(value, G_VARIANT_TYPE_DOUBLE)) {
return g_variant_get_double(value);
} else {
assert(FALSE && "not reached");
}
return 0.0;
}
static GVariant *infixfn_add(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs == 1) {
// unary addition
if (!is_valid_numeric_type(args[0])) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand type for unary +: '%s'",
g_variant_get_type_string(args[0]));
return NULL;
}
g_variant_ref(args[0]);
return args[0];
}
if (nargs != 2) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Addition takes two arguments (got %d). This is a bug in Playerctl.", nargs);
return NULL;
}
if (args[0] == NULL || args[1] == NULL) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand type for +: NULL");
return NULL;
}
if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand types for +: '%s' and '%s'",
g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1]));
return NULL;
}
if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) &&
g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) {
gint64 val0 = g_variant_get_int64(args[0]);
gint64 val1 = g_variant_get_int64(args[1]);
gint64 result = val0 + val1;
if ((val0 > 0 && val1 > 0 && result < 0) || (val0 < 0 && val1 < 0 && result > 0)) {
g_set_error(error, playerctl_formatter_error_quark(), 1, "Numeric overflow detected");
return NULL;
}
return g_variant_new("x", result);
}
gdouble val0 = get_double_value(args[0]);
gdouble val1 = get_double_value(args[1]);
gdouble result = val0 + val1;
return g_variant_new("d", result);
}
static GVariant *infixfn_sub(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs == 1) {
// unary addition
if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64)) {
gint64 value = g_variant_get_int64(args[0]);
return g_variant_new("x", value * -1);
} else if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_DOUBLE)) {
gdouble value = g_variant_get_double(args[0]);
return g_variant_new("d", value * -1);
} else {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand type for unary -: '%s'",
g_variant_get_type_string(args[0]));
return NULL;
}
}
if (nargs != 2) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Subtraction takes two arguments (got %d). This is a bug in Playerctl.", nargs);
return NULL;
}
if (args[0] == NULL || args[1] == NULL) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand type for -: NULL");
return NULL;
}
if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand types for -: '%s' and '%s'",
g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1]));
return NULL;
}
if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) &&
g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) {
gint64 val0 = g_variant_get_int64(args[0]);
gint64 val1 = g_variant_get_int64(args[1]);
gint64 result = val0 - val1;
if ((val0 > 0 && val1 < 0 && result < 0) || (val0 < 0 && val1 > 0 && result > 0)) {
g_set_error(error, playerctl_formatter_error_quark(), 1, "Numeric overflow detected");
return NULL;
}
return g_variant_new("x", result);
}
gdouble val0 = get_double_value(args[0]);
gdouble val1 = get_double_value(args[1]);
gdouble result = val0 - val1;
return g_variant_new("d", result);
}
static GVariant *infixfn_mul(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs != 2) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Multiplication takes two arguments (got %d). This is a bug in Playerctl.",
nargs);
return NULL;
}
if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand types for *: '%s' and '%s'",
g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1]));
return NULL;
}
if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) &&
g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) {
gint64 val0 = g_variant_get_int64(args[0]);
gint64 val1 = g_variant_get_int64(args[1]);
gint64 result = val0 * val1;
if (val0 != 0 && val1 / val0 != val1) {
g_set_error(error, playerctl_formatter_error_quark(), 1, "Numeric overflow detected");
return NULL;
}
return g_variant_new("x", result);
}
gdouble val0 = get_double_value(args[0]);
gdouble val1 = get_double_value(args[1]);
gdouble result = val0 * val1;
return g_variant_new("d", result);
}
static GVariant *infixfn_div(struct token *token, GVariant **args, int nargs, GError **error) {
if (nargs != 2) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Division takes two arguments (got %d). This is a bug in Playerctl.", nargs);
return NULL;
}
if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) {
g_set_error(error, playerctl_formatter_error_quark(), 1,
"Got unsupported operand types for /: '%s' and '%s'",
g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1]));
return NULL;
}
if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) &&
g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) {
gint64 val0 = g_variant_get_int64(args[0]);
gint64 val1 = g_variant_get_int64(args[1]);
if (val1 == 0) {
g_set_error(error, playerctl_formatter_error_quark(), 1, "Divide by zero error");
return NULL;
}
gint64 result = val0 / val1;
return g_variant_new("x", result);
}
gdouble val0 = get_double_value(args[0]);
gdouble val1 = get_double_value(args[1]);
if (val1 == 0.0) {
g_set_error(error, playerctl_formatter_error_quark(), 1, "Divide by zero error");
return NULL;
}
gdouble result = val0 / val1;
return g_variant_new("d", result);
}
struct template_function {
const gchar *name;
GVariant *(*func)(struct token *token, GVariant **args, int nargs, GError **error);
} template_functions[] = {
{"lc", &helperfn_lc},
{"uc", &helperfn_uc},
{"duration", &helperfn_duration},
{"markup_escape", &helperfn_markup_escape},
{"default", &helperfn_default},
{"emoji", &helperfn_emoji},
{"trunc", &helperfn_trunc},
{INFIX_ADD, &infixfn_add},
{INFIX_SUB, &infixfn_sub},
{INFIX_MUL, &infixfn_mul},
{INFIX_DIV, &infixfn_div},
};
static GVariant *expand_token(struct token *token, GVariantDict *context, GError **error) {
GError *tmp_error = NULL;
switch (token->type) {
case TOKEN_STRING:
return g_variant_new("s", token->data);
case TOKEN_NUMBER:
return g_variant_new("d", token->numeric_data);
case TOKEN_VARIABLE:
if (g_variant_dict_contains(context, token->data)) {
return g_variant_dict_lookup_value(context, token->data, NULL);
} else {
return NULL;
}
case TOKEN_FUNCTION: {
// TODO lift required arg assumption
assert(token->args != NULL);
GVariant *ret = NULL;
int nargs = 0;
GVariant *args[MAX_ARGS + 1];
GList *t;
for (t = token->args; t != NULL; t = t->next) {
struct token *arg_token = t->data;
assert(nargs < MAX_ARGS);
args[nargs++] = expand_token(arg_token, context, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
goto func_out;
}
}
for (gsize i = 0; i < LENGTH(template_functions); ++i) {
if (g_strcmp0(template_functions[i].name, token->data) == 0) {
ret = template_functions[i].func(token, args, nargs, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
goto func_out;
}
goto func_out;
}
}
g_set_error(error, playerctl_formatter_error_quark(), 1, "unknown template function: %s",
token->data);
func_out:
for (int i = 0; i < nargs; ++i) {
if (args[i] != NULL) {
g_variant_unref(args[i]);
}
}
return ret;
}
}
assert(FALSE && "not reached");
return NULL;
}
static gchar *expand_format(GList *tokens, GVariantDict *context, GError **error) {
GError *tmp_error = NULL;
GString *expanded;
expanded = g_string_new("");
GList *t = tokens;
for (t = tokens; t != NULL; t = t->next) {
GVariant *value = expand_token(t->data, context, &tmp_error);
if (tmp_error != NULL) {
g_propagate_error(error, tmp_error);
return NULL;
}
if (value != NULL) {
gchar *result = pctl_print_gvariant(value);
expanded = g_string_append(expanded, result);
g_free(result);
g_variant_unref(value);
}
}
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;
}