Permalink
Cannot retrieve contributors at this time
#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; | |
} |