mirror of
https://github.com/lbonn/rofi
synced 2024-12-22 10:13:12 +00:00
1322 lines
37 KiB
C
1322 lines
37 KiB
C
/*
|
|
* rofi
|
|
*
|
|
* MIT/X11 License
|
|
* Copyright © 2012 Sean Pringle <sean.pringle@gmail.com>
|
|
* Copyright © 2013-2022 Qball Cow <qball@gmpclient.org>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
/** The log domain for this helper. */
|
|
#define G_LOG_DOMAIN "Helper"
|
|
|
|
#include "config.h"
|
|
#include "display.h"
|
|
#include "helper-theme.h"
|
|
#include "rofi.h"
|
|
#include "settings.h"
|
|
#include "view.h"
|
|
#include "xcb.h"
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <glib.h>
|
|
#include <glib/gstdio.h>
|
|
#include <limits.h>
|
|
#include <pango/pango-fontmap.h>
|
|
#include <pango/pango.h>
|
|
#include <pango/pangocairo.h>
|
|
#include <pwd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/file.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
/**
|
|
* Textual description of positioning rofi.
|
|
*/
|
|
const char *const monitor_position_entries[] = {
|
|
"on focused monitor", "on focused window", "at mouse pointer",
|
|
"on monitor with focused window", "on monitor that has mouse pointer"};
|
|
/** copy of the argc for use in commandline argument parser. */
|
|
int stored_argc = 0;
|
|
/** copy of the argv pointer for use in the commandline argument parser */
|
|
char **stored_argv = NULL;
|
|
|
|
char *helper_string_replace_if_exists_v(char *string, GHashTable *h);
|
|
|
|
void cmd_set_arguments(int argc, char **argv) {
|
|
stored_argc = argc;
|
|
stored_argv = argv;
|
|
}
|
|
|
|
int helper_parse_setup(char *string, char ***output, int *length, ...) {
|
|
GError *error = NULL;
|
|
GHashTable *h;
|
|
h = g_hash_table_new(g_str_hash, g_str_equal);
|
|
// By default, we insert terminal and ssh-client
|
|
g_hash_table_insert(h, "{terminal}", config.terminal_emulator);
|
|
g_hash_table_insert(h, "{ssh-client}", config.ssh_client);
|
|
// Add list from variable arguments.
|
|
va_list ap;
|
|
va_start(ap, length);
|
|
while (1) {
|
|
char *key = va_arg(ap, char *);
|
|
if (key == (char *)0) {
|
|
break;
|
|
}
|
|
char *value = va_arg(ap, char *);
|
|
if (value == (char *)0) {
|
|
break;
|
|
}
|
|
g_hash_table_insert(h, key, value);
|
|
}
|
|
va_end(ap);
|
|
|
|
char *res = helper_string_replace_if_exists_v(string, h);
|
|
// Destroy key-value storage.
|
|
g_hash_table_destroy(h);
|
|
// Parse the string into shell arguments.
|
|
if (g_shell_parse_argv(res, length, output, &error)) {
|
|
g_free(res);
|
|
return TRUE;
|
|
}
|
|
g_free(res);
|
|
// Throw error if shell parsing fails.
|
|
if (error) {
|
|
char *msg = g_strdup_printf("Failed to parse: '%s'\nError: '%s'", string,
|
|
error->message);
|
|
rofi_view_error_dialog(msg, FALSE);
|
|
g_free(msg);
|
|
// print error.
|
|
g_error_free(error);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
void helper_tokenize_free(rofi_int_matcher **tokens) {
|
|
for (size_t i = 0; tokens && tokens[i]; i++) {
|
|
g_regex_unref((GRegex *)tokens[i]->regex);
|
|
g_free(tokens[i]);
|
|
}
|
|
g_free(tokens);
|
|
}
|
|
|
|
static gchar *glob_to_regex(const char *input) {
|
|
gchar *r = g_regex_escape_string(input, -1);
|
|
size_t str_l = strlen(r);
|
|
for (size_t i = 0; i < str_l; i++) {
|
|
if (r[i] == '\\') {
|
|
if (r[i + 1] == '*') {
|
|
r[i] = '.';
|
|
} else if (r[i + 1] == '?') {
|
|
r[i + 1] = 'S';
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
static gchar *fuzzy_to_regex(const char *input) {
|
|
GString *str = g_string_new("");
|
|
gchar *r = g_regex_escape_string(input, -1);
|
|
gchar *iter;
|
|
int first = 1;
|
|
for (iter = r; iter && *iter != '\0'; iter = g_utf8_next_char(iter)) {
|
|
if (first) {
|
|
g_string_append(str, "(");
|
|
} else {
|
|
g_string_append(str, ".*?(");
|
|
}
|
|
if (*iter == '\\') {
|
|
g_string_append_c(str, '\\');
|
|
iter = g_utf8_next_char(iter);
|
|
// If EOL, break out of for loop.
|
|
if ((*iter) == '\0') {
|
|
break;
|
|
}
|
|
}
|
|
g_string_append_unichar(str, g_utf8_get_char(iter));
|
|
g_string_append(str, ")");
|
|
first = 0;
|
|
}
|
|
g_free(r);
|
|
char *retv = str->str;
|
|
g_string_free(str, FALSE);
|
|
return retv;
|
|
}
|
|
|
|
static gchar *prefix_regex(const char *input) {
|
|
gchar *r = g_regex_escape_string(input, -1);
|
|
char *retv = g_strconcat("\\b", r, NULL);
|
|
g_free(r);
|
|
return retv;
|
|
}
|
|
|
|
static char *utf8_helper_simplify_string(const char *s) {
|
|
gunichar buf2[G_UNICHAR_MAX_DECOMPOSITION_LENGTH] = {
|
|
0,
|
|
};
|
|
char buf[6] = {
|
|
0,
|
|
};
|
|
// Compose the string in maximally composed form.
|
|
ssize_t str_size = (g_utf8_strlen(s, -1) * 6 + 2 + 1) * sizeof(char);
|
|
char *str = g_malloc0(str_size);
|
|
char *striter = str;
|
|
for (const char *iter = s; iter && *iter; iter = g_utf8_next_char(iter)) {
|
|
gunichar uc = g_utf8_get_char(iter);
|
|
int l = 0;
|
|
gsize dl = g_unichar_fully_decompose(uc, FALSE, buf2,
|
|
G_UNICHAR_MAX_DECOMPOSITION_LENGTH);
|
|
if (dl) {
|
|
l = g_unichar_to_utf8(buf2[0], buf);
|
|
} else {
|
|
l = g_unichar_to_utf8(uc, buf);
|
|
}
|
|
memcpy(striter, buf, l);
|
|
striter += l;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
// Macro for quickly generating regex for matching.
|
|
static inline GRegex *R(const char *s, int case_sensitive) {
|
|
if (config.normalize_match) {
|
|
char *str = utf8_helper_simplify_string(s);
|
|
|
|
GRegex *r = g_regex_new(
|
|
str, G_REGEX_OPTIMIZE | ((case_sensitive) ? 0 : G_REGEX_CASELESS), 0,
|
|
NULL);
|
|
|
|
g_free(str);
|
|
return r;
|
|
}
|
|
return g_regex_new(
|
|
s, G_REGEX_OPTIMIZE | ((case_sensitive) ? 0 : G_REGEX_CASELESS), 0, NULL);
|
|
}
|
|
|
|
static rofi_int_matcher *create_regex(const char *input, int case_sensitive) {
|
|
GRegex *retv = NULL;
|
|
gchar *r;
|
|
rofi_int_matcher *rv = g_malloc0(sizeof(rofi_int_matcher));
|
|
if (input && input[0] == config.matching_negate_char) {
|
|
rv->invert = 1;
|
|
input++;
|
|
}
|
|
switch (config.matching_method) {
|
|
case MM_GLOB:
|
|
r = glob_to_regex(input);
|
|
retv = R(r, case_sensitive);
|
|
g_free(r);
|
|
break;
|
|
case MM_REGEX:
|
|
retv = R(input, case_sensitive);
|
|
if (retv == NULL) {
|
|
r = g_regex_escape_string(input, -1);
|
|
retv = R(r, case_sensitive);
|
|
g_free(r);
|
|
}
|
|
break;
|
|
case MM_FUZZY:
|
|
r = fuzzy_to_regex(input);
|
|
retv = R(r, case_sensitive);
|
|
g_free(r);
|
|
break;
|
|
case MM_PREFIX:
|
|
r = prefix_regex(input);
|
|
retv = R(r, case_sensitive);
|
|
g_free(r);
|
|
break;
|
|
default:
|
|
r = g_regex_escape_string(input, -1);
|
|
retv = R(r, case_sensitive);
|
|
g_free(r);
|
|
break;
|
|
}
|
|
rv->regex = retv;
|
|
return rv;
|
|
}
|
|
rofi_int_matcher **helper_tokenize(const char *input, int case_sensitive) {
|
|
if (input == NULL) {
|
|
return NULL;
|
|
}
|
|
size_t len = strlen(input);
|
|
if (len == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
char *saveptr = NULL, *token;
|
|
rofi_int_matcher **retv = NULL;
|
|
if (!config.tokenize) {
|
|
retv = g_malloc0(sizeof(rofi_int_matcher *) * 2);
|
|
retv[0] = create_regex(input, case_sensitive);
|
|
return retv;
|
|
}
|
|
|
|
// First entry is always full (modified) stringtext.
|
|
int num_tokens = 0;
|
|
|
|
// Copy the string, 'strtok_r' modifies it.
|
|
char *str = g_strdup(input);
|
|
|
|
// Iterate over tokens.
|
|
// strtok should still be valid for utf8.
|
|
const char *const sep = " ";
|
|
for (token = strtok_r(str, sep, &saveptr); token != NULL;
|
|
token = strtok_r(NULL, sep, &saveptr)) {
|
|
retv = g_realloc(retv, sizeof(rofi_int_matcher *) * (num_tokens + 2));
|
|
retv[num_tokens] = create_regex(token, case_sensitive);
|
|
retv[num_tokens + 1] = NULL;
|
|
num_tokens++;
|
|
}
|
|
// Free str.
|
|
g_free(str);
|
|
return retv;
|
|
}
|
|
|
|
// cli arg handling
|
|
int find_arg(const char *const key) {
|
|
int i;
|
|
|
|
for (i = 0; i < stored_argc && strcasecmp(stored_argv[i], key); i++) {
|
|
;
|
|
}
|
|
|
|
return i < stored_argc ? i : -1;
|
|
}
|
|
int find_arg_str(const char *const key, char **val) {
|
|
int i = find_arg(key);
|
|
|
|
if (val != NULL && i > 0 && i < stored_argc - 1) {
|
|
*val = stored_argv[i + 1];
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
const char **find_arg_strv(const char *const key) {
|
|
const char **retv = NULL;
|
|
int length = 0;
|
|
for (int i = 0; i < stored_argc; i++) {
|
|
if (i < (stored_argc - 1) && strcasecmp(stored_argv[i], key) == 0) {
|
|
length++;
|
|
}
|
|
}
|
|
if (length > 0) {
|
|
retv = g_malloc0((length + 1) * sizeof(char *));
|
|
int index = 0;
|
|
for (int i = 0; i < stored_argc; i++) {
|
|
if (i < (stored_argc - 1) && strcasecmp(stored_argv[i], key) == 0) {
|
|
retv[index++] = stored_argv[i + 1];
|
|
}
|
|
}
|
|
}
|
|
return retv;
|
|
}
|
|
|
|
int find_arg_int(const char *const key, int *val) {
|
|
int i = find_arg(key);
|
|
|
|
if (val != NULL && i > 0 && i < (stored_argc - 1)) {
|
|
*val = strtol(stored_argv[i + 1], NULL, 10);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
int find_arg_uint(const char *const key, unsigned int *val) {
|
|
int i = find_arg(key);
|
|
|
|
if (val != NULL && i > 0 && i < (stored_argc - 1)) {
|
|
*val = strtoul(stored_argv[i + 1], NULL, 10);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
char helper_parse_char(const char *arg) {
|
|
const size_t len = strlen(arg);
|
|
// If the length is 1, it is not escaped.
|
|
if (len == 1) {
|
|
return arg[0];
|
|
}
|
|
// If the length is 2 and the first character is '\', we unescape it.
|
|
if (len == 2 && arg[0] == '\\') {
|
|
switch (arg[1]) {
|
|
// New line
|
|
case 'n':
|
|
return '\n';
|
|
// Bell
|
|
case 'a':
|
|
return '\a';
|
|
// Backspace
|
|
case 'b':
|
|
return '\b';
|
|
// Tab
|
|
case 't':
|
|
return '\t';
|
|
// Vertical tab
|
|
case 'v':
|
|
return '\v';
|
|
// Form feed
|
|
case 'f':
|
|
return '\f';
|
|
// Carriage return
|
|
case 'r':
|
|
return '\r';
|
|
// Forward slash
|
|
case '\\':
|
|
return '\\';
|
|
// 0 line.
|
|
case '0':
|
|
return '\0';
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (len > 2 && arg[0] == '\\' && arg[1] == 'x') {
|
|
return (char)strtol(&arg[2], NULL, 16);
|
|
}
|
|
g_warning("Failed to parse character string: \"%s\"", arg);
|
|
// for now default to newline.
|
|
return '\n';
|
|
}
|
|
|
|
int find_arg_char(const char *const key, char *val) {
|
|
int i = find_arg(key);
|
|
|
|
if (val != NULL && i > 0 && i < (stored_argc - 1)) {
|
|
*val = helper_parse_char(stored_argv[i + 1]);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
PangoAttrList *helper_token_match_get_pango_attr(RofiHighlightColorStyle th,
|
|
rofi_int_matcher **tokens,
|
|
const char *input,
|
|
PangoAttrList *retv) {
|
|
// Disable highlighting for normalize match, not supported atm.
|
|
if (config.normalize_match) {
|
|
return retv;
|
|
}
|
|
// Do a tokenized match.
|
|
if (tokens) {
|
|
for (int j = 0; tokens[j]; j++) {
|
|
GMatchInfo *gmi = NULL;
|
|
if (tokens[j]->invert) {
|
|
continue;
|
|
}
|
|
g_regex_match(tokens[j]->regex, input, G_REGEX_MATCH_PARTIAL, &gmi);
|
|
while (g_match_info_matches(gmi)) {
|
|
int count = g_match_info_get_match_count(gmi);
|
|
for (int index = (count > 1) ? 1 : 0; index < count; index++) {
|
|
int start, end;
|
|
g_match_info_fetch_pos(gmi, index, &start, &end);
|
|
if (th.style & ROFI_HL_BOLD) {
|
|
PangoAttribute *pa = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
|
|
pa->start_index = start;
|
|
pa->end_index = end;
|
|
pango_attr_list_insert(retv, pa);
|
|
}
|
|
if (th.style & ROFI_HL_UNDERLINE) {
|
|
PangoAttribute *pa =
|
|
pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
|
|
pa->start_index = start;
|
|
pa->end_index = end;
|
|
pango_attr_list_insert(retv, pa);
|
|
}
|
|
if (th.style & ROFI_HL_STRIKETHROUGH) {
|
|
PangoAttribute *pa = pango_attr_strikethrough_new(TRUE);
|
|
pa->start_index = start;
|
|
pa->end_index = end;
|
|
pango_attr_list_insert(retv, pa);
|
|
}
|
|
if (th.style & ROFI_HL_SMALL_CAPS) {
|
|
PangoAttribute *pa =
|
|
pango_attr_variant_new(PANGO_VARIANT_SMALL_CAPS);
|
|
pa->start_index = start;
|
|
pa->end_index = end;
|
|
pango_attr_list_insert(retv, pa);
|
|
}
|
|
if (th.style & ROFI_HL_ITALIC) {
|
|
PangoAttribute *pa = pango_attr_style_new(PANGO_STYLE_ITALIC);
|
|
pa->start_index = start;
|
|
pa->end_index = end;
|
|
pango_attr_list_insert(retv, pa);
|
|
}
|
|
if (th.style & ROFI_HL_COLOR) {
|
|
PangoAttribute *pa = pango_attr_foreground_new(
|
|
th.color.red * 65535, th.color.green * 65535,
|
|
th.color.blue * 65535);
|
|
pa->start_index = start;
|
|
pa->end_index = end;
|
|
pango_attr_list_insert(retv, pa);
|
|
|
|
if (th.color.alpha < 1.0) {
|
|
pa = pango_attr_foreground_alpha_new(th.color.alpha * 65535);
|
|
pa->start_index = start;
|
|
pa->end_index = end;
|
|
pango_attr_list_insert(retv, pa);
|
|
}
|
|
}
|
|
}
|
|
g_match_info_next(gmi, NULL);
|
|
}
|
|
g_match_info_free(gmi);
|
|
}
|
|
}
|
|
return retv;
|
|
}
|
|
|
|
int helper_token_match(rofi_int_matcher *const *tokens, const char *input) {
|
|
int match = TRUE;
|
|
// Do a tokenized match.
|
|
if (tokens) {
|
|
if (config.normalize_match) {
|
|
char *r = utf8_helper_simplify_string(input);
|
|
for (int j = 0; match && tokens[j]; j++) {
|
|
match = g_regex_match(tokens[j]->regex, r, 0, NULL);
|
|
match ^= tokens[j]->invert;
|
|
}
|
|
g_free(r);
|
|
} else {
|
|
for (int j = 0; match && tokens[j]; j++) {
|
|
match = g_regex_match(tokens[j]->regex, input, 0, NULL);
|
|
match ^= tokens[j]->invert;
|
|
}
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
int execute_generator(const char *cmd) {
|
|
char **args = NULL;
|
|
int argv = 0;
|
|
helper_parse_setup(config.run_command, &args, &argv, "{cmd}", cmd, (char *)0);
|
|
|
|
int fd = -1;
|
|
GError *error = NULL;
|
|
g_spawn_async_with_pipes(NULL, args, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
|
|
NULL, NULL, &fd, NULL, &error);
|
|
|
|
if (error != NULL) {
|
|
char *msg = g_strdup_printf("Failed to execute: '%s'\nError: '%s'", cmd,
|
|
error->message);
|
|
rofi_view_error_dialog(msg, FALSE);
|
|
g_free(msg);
|
|
// print error.
|
|
g_error_free(error);
|
|
fd = -1;
|
|
}
|
|
g_strfreev(args);
|
|
return fd;
|
|
}
|
|
|
|
int create_pid_file(const char *pidfile, gboolean kill_running) {
|
|
if (pidfile == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
int fd = g_open(pidfile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
|
|
if (fd < 0) {
|
|
g_warning("Failed to create pid file: '%s'.", pidfile);
|
|
return -1;
|
|
}
|
|
// Set it to close the File Descriptor on exit.
|
|
int flags = fcntl(fd, F_GETFD, NULL);
|
|
flags = flags | FD_CLOEXEC;
|
|
if (fcntl(fd, F_SETFD, flags, NULL) < 0) {
|
|
g_warning("Failed to set CLOEXEC on pidfile.");
|
|
remove_pid_file(fd);
|
|
return -1;
|
|
}
|
|
// Try to get exclusive write lock on FD
|
|
int retv = flock(fd, LOCK_EX | LOCK_NB);
|
|
if (retv != 0) {
|
|
g_warning("Failed to set lock on pidfile: Rofi already running?");
|
|
g_warning("Got error: %d %s", retv, g_strerror(errno));
|
|
if (kill_running) {
|
|
char buffer[64] = {
|
|
0,
|
|
};
|
|
ssize_t l = read(fd, &buffer, 64);
|
|
if (l > 1) {
|
|
pid_t pid = g_ascii_strtoll(buffer, NULL, 0);
|
|
kill(pid, SIGTERM);
|
|
while (1) {
|
|
retv = flock(fd, LOCK_EX | LOCK_NB);
|
|
if (retv == 0) {
|
|
break;
|
|
}
|
|
g_usleep(100);
|
|
}
|
|
}
|
|
remove_pid_file(fd);
|
|
return create_pid_file(pidfile, FALSE);
|
|
}
|
|
|
|
remove_pid_file(fd);
|
|
return -1;
|
|
}
|
|
if (ftruncate(fd, (off_t)0) == 0) {
|
|
// Write pid, not needed, but for completeness sake.
|
|
char buffer[64];
|
|
int length = snprintf(buffer, 64, "%i", getpid());
|
|
ssize_t l = 0;
|
|
while (l < length) {
|
|
l += write(fd, &buffer[l], length - l);
|
|
}
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
void remove_pid_file(int fd) {
|
|
if (fd >= 0) {
|
|
if (close(fd)) {
|
|
g_warning("Failed to close pidfile: '%s'", g_strerror(errno));
|
|
}
|
|
}
|
|
}
|
|
|
|
gboolean helper_validate_font(PangoFontDescription *pfd, const char *font) {
|
|
const char *fam = pango_font_description_get_family(pfd);
|
|
int size = pango_font_description_get_size(pfd);
|
|
if (fam == NULL || size == 0) {
|
|
g_debug("Pango failed to parse font: '%s'", font);
|
|
g_debug("Got family: <b>%s</b> at size: <b>%d</b>", fam ? fam : "{unknown}",
|
|
size);
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Do some input validation, especially the first few could break things.
|
|
* It is good to catch them beforehand.
|
|
*
|
|
* This functions exits the program with 1 when it finds an invalid
|
|
* configuration.
|
|
*/
|
|
int config_sanity_check(void) {
|
|
int found_error = FALSE;
|
|
GString *msg =
|
|
g_string_new("<big><b>The configuration failed to validate:</b></big>\n");
|
|
|
|
if (config.sorting_method) {
|
|
if (g_strcmp0(config.sorting_method, "normal") == 0) {
|
|
config.sorting_method_enum = SORT_NORMAL;
|
|
} else if (g_strcmp0(config.sorting_method, "levenshtein") == 0) {
|
|
config.sorting_method_enum = SORT_NORMAL;
|
|
} else if (g_strcmp0(config.sorting_method, "fzf") == 0) {
|
|
config.sorting_method_enum = SORT_FZF;
|
|
} else {
|
|
g_string_append_printf(
|
|
msg,
|
|
"\t<b>config.sorting_method</b>=%s is not a valid sorting "
|
|
"strategy.\nValid options are: normal or fzf.\n",
|
|
config.sorting_method);
|
|
found_error = 1;
|
|
}
|
|
}
|
|
|
|
if (config.matching) {
|
|
if (g_strcmp0(config.matching, "regex") == 0) {
|
|
config.matching_method = MM_REGEX;
|
|
} else if (g_strcmp0(config.matching, "glob") == 0) {
|
|
config.matching_method = MM_GLOB;
|
|
} else if (g_strcmp0(config.matching, "fuzzy") == 0) {
|
|
config.matching_method = MM_FUZZY;
|
|
} else if (g_strcmp0(config.matching, "normal") == 0) {
|
|
config.matching_method = MM_NORMAL;
|
|
;
|
|
} else if (g_strcmp0(config.matching, "prefix") == 0) {
|
|
config.matching_method = MM_PREFIX;
|
|
} else {
|
|
g_string_append_printf(msg,
|
|
"\t<b>config.matching</b>=%s is not a valid "
|
|
"matching strategy.\nValid options are: glob, "
|
|
"regex, fuzzy, prefix or normal.\n",
|
|
config.matching);
|
|
found_error = 1;
|
|
}
|
|
}
|
|
|
|
if (config.element_height < 1) {
|
|
g_string_append_printf(msg,
|
|
"\t<b>config.element_height</b>=%d is invalid. An "
|
|
"element needs to be at least 1 line high.\n",
|
|
config.element_height);
|
|
config.element_height = 1;
|
|
found_error = TRUE;
|
|
}
|
|
if (!(config.location >= 0 && config.location <= 8)) {
|
|
g_string_append_printf(msg,
|
|
"\t<b>config.location</b>=%d is invalid. Value "
|
|
"should be between %d and %d.\n",
|
|
config.location, 0, 8);
|
|
config.location = WL_CENTER;
|
|
found_error = 1;
|
|
}
|
|
|
|
// Check size
|
|
{
|
|
workarea mon;
|
|
if (!monitor_active(&mon)) {
|
|
const char *name = config.monitor;
|
|
if (name && name[0] == '-') {
|
|
int index = name[1] - '0';
|
|
if (index < 5 && index > 0) {
|
|
name = monitor_position_entries[index - 1];
|
|
}
|
|
}
|
|
g_string_append_printf(
|
|
msg, "\t<b>config.monitor</b>=%s Could not find monitor.\n", name);
|
|
found_error = TRUE;
|
|
}
|
|
}
|
|
|
|
if (g_strcmp0(config.monitor, "-3") == 0) {
|
|
// On -3, set to location 1.
|
|
config.location = 1;
|
|
}
|
|
|
|
if (found_error) {
|
|
g_string_append(msg, "Please update your configuration.");
|
|
rofi_add_error_message(msg);
|
|
return TRUE;
|
|
}
|
|
|
|
g_string_free(msg, TRUE);
|
|
return FALSE;
|
|
}
|
|
|
|
char *rofi_expand_path(const char *input) {
|
|
char **str = g_strsplit(input, G_DIR_SEPARATOR_S, -1);
|
|
for (unsigned int i = 0; str && str[i]; i++) {
|
|
// Replace ~ with current user homedir.
|
|
if (str[i][0] == '~' && str[i][1] == '\0') {
|
|
g_free(str[i]);
|
|
str[i] = g_strdup(g_get_home_dir());
|
|
}
|
|
// If other user, ask getpwnam.
|
|
else if (str[i][0] == '~') {
|
|
struct passwd *p = getpwnam(&(str[i][1]));
|
|
if (p != NULL) {
|
|
g_free(str[i]);
|
|
str[i] = g_strdup(p->pw_dir);
|
|
}
|
|
} else if (i == 0) {
|
|
char *s = str[i];
|
|
if (input[0] == G_DIR_SEPARATOR) {
|
|
str[i] = g_strdup_printf("%s%s", G_DIR_SEPARATOR_S, s);
|
|
g_free(s);
|
|
}
|
|
}
|
|
}
|
|
char *retv = g_build_filenamev(str);
|
|
g_strfreev(str);
|
|
return retv;
|
|
}
|
|
|
|
/** Return the minimum value of a,b,c */
|
|
#define MIN3(a, b, c) \
|
|
((a) < (b) ? ((a) < (c) ? (a) : (c)) : ((b) < (c) ? (b) : (c)))
|
|
|
|
unsigned int levenshtein(const char *needle, const glong needlelen,
|
|
const char *haystack, const glong haystacklen) {
|
|
if (needlelen == G_MAXLONG) {
|
|
// String to long, we cannot handle this.
|
|
return UINT_MAX;
|
|
}
|
|
unsigned int column[needlelen + 1];
|
|
for (glong y = 0; y < needlelen; y++) {
|
|
column[y] = y;
|
|
}
|
|
// Removed out of the loop, otherwise static code analyzers think it is
|
|
// unset.. silly but true. old loop: for ( glong y = 0; y <= needlelen; y++)
|
|
column[needlelen] = needlelen;
|
|
for (glong x = 1; x <= haystacklen; x++) {
|
|
const char *needles = needle;
|
|
column[0] = x;
|
|
gunichar haystackc = g_utf8_get_char(haystack);
|
|
if (!config.case_sensitive) {
|
|
haystackc = g_unichar_tolower(haystackc);
|
|
}
|
|
for (glong y = 1, lastdiag = x - 1; y <= needlelen; y++) {
|
|
gunichar needlec = g_utf8_get_char(needles);
|
|
if (!config.case_sensitive) {
|
|
needlec = g_unichar_tolower(needlec);
|
|
}
|
|
unsigned int olddiag = column[y];
|
|
column[y] = MIN3(column[y] + 1, column[y - 1] + 1,
|
|
lastdiag + (needlec == haystackc ? 0 : 1));
|
|
lastdiag = olddiag;
|
|
needles = g_utf8_next_char(needles);
|
|
}
|
|
haystack = g_utf8_next_char(haystack);
|
|
}
|
|
return column[needlelen];
|
|
}
|
|
|
|
char *rofi_latin_to_utf8_strdup(const char *input, gssize length) {
|
|
gsize slength = 0;
|
|
return g_convert_with_fallback(input, length, "UTF-8", "latin1", "\uFFFD",
|
|
NULL, &slength, NULL);
|
|
}
|
|
|
|
char *rofi_force_utf8(const gchar *data, ssize_t length) {
|
|
if (data == NULL) {
|
|
return NULL;
|
|
}
|
|
const char *end;
|
|
GString *string;
|
|
|
|
if (g_utf8_validate(data, length, &end)) {
|
|
return g_memdup(data, length + 1);
|
|
}
|
|
string = g_string_sized_new(length + 16);
|
|
|
|
do {
|
|
/* Valid part of the string */
|
|
g_string_append_len(string, data, end - data);
|
|
/* Replacement character */
|
|
g_string_append(string, "\uFFFD");
|
|
length -= (end - data) + 1;
|
|
data = end + 1;
|
|
} while (!g_utf8_validate(data, length, &end));
|
|
|
|
if (length) {
|
|
g_string_append_len(string, data, length);
|
|
}
|
|
|
|
return g_string_free(string, FALSE);
|
|
}
|
|
|
|
/****
|
|
* FZF like scorer
|
|
*/
|
|
|
|
/** Max length of input to score. */
|
|
#define FUZZY_SCORER_MAX_LENGTH 256
|
|
/** minimum score */
|
|
#define MIN_SCORE (INT_MIN / 2)
|
|
/** Leading gap score */
|
|
#define LEADING_GAP_SCORE -4
|
|
/** gap score */
|
|
#define GAP_SCORE -5
|
|
/** start of word score */
|
|
#define WORD_START_SCORE 50
|
|
/** non-word score */
|
|
#define NON_WORD_SCORE 40
|
|
/** CamelCase score */
|
|
#define CAMEL_SCORE (WORD_START_SCORE + GAP_SCORE - 1)
|
|
/** Consecutive score */
|
|
#define CONSECUTIVE_SCORE (WORD_START_SCORE + GAP_SCORE)
|
|
/** non-start multiplier */
|
|
#define PATTERN_NON_START_MULTIPLIER 1
|
|
/** start multiplier */
|
|
#define PATTERN_START_MULTIPLIER 2
|
|
|
|
/**
|
|
* Character classification.
|
|
*/
|
|
enum CharClass {
|
|
/* Lower case */
|
|
LOWER,
|
|
/* Upper case */
|
|
UPPER,
|
|
/* Number */
|
|
DIGIT,
|
|
/* non word character */
|
|
NON_WORD
|
|
};
|
|
|
|
/**
|
|
* @param c The character to determine class of
|
|
*
|
|
* @returns the class of the character c.
|
|
*/
|
|
static enum CharClass rofi_scorer_get_character_class(gunichar c) {
|
|
if (g_unichar_islower(c)) {
|
|
return LOWER;
|
|
}
|
|
if (g_unichar_isupper(c)) {
|
|
return UPPER;
|
|
}
|
|
if (g_unichar_isdigit(c)) {
|
|
return DIGIT;
|
|
}
|
|
return NON_WORD;
|
|
}
|
|
|
|
/**
|
|
* @param prev The previous character.
|
|
* @param curr The current character
|
|
*
|
|
* Scrore the transition.
|
|
*
|
|
* @returns score of the transition.
|
|
*/
|
|
static int rofi_scorer_get_score_for(enum CharClass prev, enum CharClass curr) {
|
|
if (prev == NON_WORD && curr != NON_WORD) {
|
|
return WORD_START_SCORE;
|
|
}
|
|
if ((prev == LOWER && curr == UPPER) || (prev != DIGIT && curr == DIGIT)) {
|
|
return CAMEL_SCORE;
|
|
}
|
|
if (curr == NON_WORD) {
|
|
return NON_WORD_SCORE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int rofi_scorer_fuzzy_evaluate(const char *pattern, glong plen, const char *str,
|
|
glong slen) {
|
|
if (slen > FUZZY_SCORER_MAX_LENGTH) {
|
|
return -MIN_SCORE;
|
|
}
|
|
glong pi, si;
|
|
// whether we are aligning the first character of pattern
|
|
gboolean pfirst = TRUE;
|
|
// whether the start of a word in pattern
|
|
gboolean pstart = TRUE;
|
|
// score for each position
|
|
int *score = g_malloc_n(slen, sizeof(int));
|
|
// dp[i]: maximum value by aligning pattern[0..pi] to str[0..si]
|
|
int *dp = g_malloc_n(slen, sizeof(int));
|
|
// uleft: value of the upper left cell; ulefts: maximum value of uleft and
|
|
// cells on the left. The arbitrary initial values suppress warnings.
|
|
int uleft = 0, ulefts = 0, left, lefts;
|
|
const gchar *pit = pattern, *sit;
|
|
enum CharClass prev = NON_WORD;
|
|
for (si = 0, sit = str; si < slen; si++, sit = g_utf8_next_char(sit)) {
|
|
enum CharClass cur = rofi_scorer_get_character_class(g_utf8_get_char(sit));
|
|
score[si] = rofi_scorer_get_score_for(prev, cur);
|
|
prev = cur;
|
|
dp[si] = MIN_SCORE;
|
|
}
|
|
for (pi = 0; pi < plen; pi++, pit = g_utf8_next_char(pit)) {
|
|
gunichar pc = g_utf8_get_char(pit), sc;
|
|
if (g_unichar_isspace(pc)) {
|
|
pstart = TRUE;
|
|
continue;
|
|
}
|
|
lefts = MIN_SCORE;
|
|
for (si = 0, sit = str; si < slen; si++, sit = g_utf8_next_char(sit)) {
|
|
left = dp[si];
|
|
lefts = MAX(lefts + GAP_SCORE, left);
|
|
sc = g_utf8_get_char(sit);
|
|
if (config.case_sensitive
|
|
? pc == sc
|
|
: g_unichar_tolower(pc) == g_unichar_tolower(sc)) {
|
|
int t = score[si] * (pstart ? PATTERN_START_MULTIPLIER
|
|
: PATTERN_NON_START_MULTIPLIER);
|
|
dp[si] = pfirst ? LEADING_GAP_SCORE * si + t
|
|
: MAX(uleft + CONSECUTIVE_SCORE, ulefts + t);
|
|
} else {
|
|
dp[si] = MIN_SCORE;
|
|
}
|
|
uleft = left;
|
|
ulefts = lefts;
|
|
}
|
|
pfirst = pstart = FALSE;
|
|
}
|
|
lefts = MIN_SCORE;
|
|
for (si = 0; si < slen; si++) {
|
|
lefts = MAX(lefts + GAP_SCORE, dp[si]);
|
|
}
|
|
g_free(score);
|
|
g_free(dp);
|
|
return -lefts;
|
|
}
|
|
|
|
/**
|
|
* @param a UTF-8 string to compare
|
|
* @param b UTF-8 string to compare
|
|
* @param n Maximum number of characters to compare
|
|
*
|
|
* Compares the `G_NORMALIZE_ALL_COMPOSE` forms of the two strings.
|
|
*
|
|
* @returns less than, equal to, or greater than zero if the first `n`
|
|
* characters (not bytes) of `a` are found, respectively, to be less than, to
|
|
* match, or be greater than the first `n` characters (not bytes) of `b`.
|
|
*/
|
|
int utf8_strncmp(const char *a, const char *b, size_t n) {
|
|
char *na = g_utf8_normalize(a, -1, G_NORMALIZE_ALL_COMPOSE);
|
|
char *nb = g_utf8_normalize(b, -1, G_NORMALIZE_ALL_COMPOSE);
|
|
*g_utf8_offset_to_pointer(na, n) = '\0';
|
|
*g_utf8_offset_to_pointer(nb, n) = '\0';
|
|
int r = g_utf8_collate(na, nb);
|
|
g_free(na);
|
|
g_free(nb);
|
|
return r;
|
|
}
|
|
|
|
gboolean helper_execute(const char *wd, char **args, const char *error_precmd,
|
|
const char *error_cmd,
|
|
RofiHelperExecuteContext *context) {
|
|
gboolean retv = TRUE;
|
|
GError *error = NULL;
|
|
|
|
GSpawnChildSetupFunc child_setup = NULL;
|
|
gpointer user_data = NULL;
|
|
|
|
display_startup_notification(context, &child_setup, &user_data);
|
|
|
|
g_spawn_async(wd, args, NULL, G_SPAWN_SEARCH_PATH, child_setup, user_data,
|
|
NULL, &error);
|
|
if (error != NULL) {
|
|
char *msg = g_strdup_printf("Failed to execute: '%s%s'\nError: '%s'",
|
|
error_precmd, error_cmd, error->message);
|
|
rofi_view_error_dialog(msg, FALSE);
|
|
g_free(msg);
|
|
// print error.
|
|
g_error_free(error);
|
|
retv = FALSE;
|
|
}
|
|
|
|
// Free the args list.
|
|
g_strfreev(args);
|
|
return retv;
|
|
}
|
|
|
|
gboolean helper_execute_command(const char *wd, const char *cmd,
|
|
gboolean run_in_term,
|
|
RofiHelperExecuteContext *context) {
|
|
char **args = NULL;
|
|
int argc = 0;
|
|
|
|
if (run_in_term) {
|
|
helper_parse_setup(config.run_shell_command, &args, &argc, "{cmd}", cmd,
|
|
(char *)0);
|
|
} else {
|
|
helper_parse_setup(config.run_command, &args, &argc, "{cmd}", cmd,
|
|
(char *)0);
|
|
}
|
|
|
|
if (args == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
if (context != NULL) {
|
|
if (context->name == NULL) {
|
|
context->name = args[0];
|
|
}
|
|
if (context->binary == NULL) {
|
|
context->binary = args[0];
|
|
}
|
|
if (context->description == NULL) {
|
|
gsize l = strlen("Launching '' via rofi") + strlen(cmd) + 1;
|
|
gchar *description = g_newa(gchar, l);
|
|
|
|
g_snprintf(description, l, "Launching '%s' via rofi", cmd);
|
|
context->description = description;
|
|
}
|
|
if (context->command == NULL) {
|
|
context->command = cmd;
|
|
}
|
|
}
|
|
|
|
return helper_execute(wd, args, "", cmd, context);
|
|
}
|
|
|
|
char *helper_get_theme_path(const char *file, const char *ext) {
|
|
char *filename = rofi_expand_path(file);
|
|
g_debug("Opening theme, testing: %s\n", filename);
|
|
if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
|
|
return filename;
|
|
}
|
|
g_free(filename);
|
|
|
|
if (g_str_has_suffix(file, ext)) {
|
|
filename = g_strdup(file);
|
|
} else {
|
|
filename = g_strconcat(file, ext, NULL);
|
|
}
|
|
// Check config's themes directory.
|
|
const char *cpath = g_get_user_config_dir();
|
|
if (cpath) {
|
|
char *themep = g_build_filename(cpath, "rofi", "themes", filename, NULL);
|
|
g_debug("Opening theme, testing: %s", themep);
|
|
if (themep && g_file_test(themep, G_FILE_TEST_EXISTS)) {
|
|
g_free(filename);
|
|
return themep;
|
|
}
|
|
g_free(themep);
|
|
}
|
|
// Check config directory.
|
|
if (cpath) {
|
|
char *themep = g_build_filename(cpath, "rofi", filename, NULL);
|
|
g_debug("Opening theme, testing: %s", themep);
|
|
if (g_file_test(themep, G_FILE_TEST_EXISTS)) {
|
|
g_free(filename);
|
|
return themep;
|
|
}
|
|
g_free(themep);
|
|
}
|
|
const char *datadir = g_get_user_data_dir();
|
|
if (datadir) {
|
|
char *theme_path =
|
|
g_build_filename(datadir, "rofi", "themes", filename, NULL);
|
|
if (theme_path) {
|
|
g_debug("Opening theme, testing: %s", theme_path);
|
|
if (g_file_test(theme_path, G_FILE_TEST_EXISTS)) {
|
|
g_free(filename);
|
|
return theme_path;
|
|
}
|
|
g_free(theme_path);
|
|
}
|
|
}
|
|
|
|
const gchar *const *system_data_dirs = g_get_system_data_dirs();
|
|
if (system_data_dirs) {
|
|
for (uint_fast32_t i = 0; system_data_dirs[i] != NULL; i++) {
|
|
const char *const sdatadir = system_data_dirs[i];
|
|
g_debug("Opening theme directory: %s", sdatadir);
|
|
char *theme_path =
|
|
g_build_filename(sdatadir, "rofi", "themes", filename, NULL);
|
|
if (theme_path) {
|
|
g_debug("Opening theme, testing: %s", theme_path);
|
|
if (g_file_test(theme_path, G_FILE_TEST_EXISTS)) {
|
|
g_free(filename);
|
|
return theme_path;
|
|
}
|
|
g_free(theme_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
char *theme_path = g_build_filename(THEME_DIR, filename, NULL);
|
|
if (theme_path) {
|
|
g_debug("Opening theme, testing: %s", theme_path);
|
|
if (g_file_test(theme_path, G_FILE_TEST_EXISTS)) {
|
|
g_free(filename);
|
|
return theme_path;
|
|
}
|
|
g_free(theme_path);
|
|
}
|
|
return filename;
|
|
}
|
|
|
|
static gboolean parse_pair(char *input, rofi_range_pair *item) {
|
|
// Skip leading blanks.
|
|
while (input != NULL && isblank(*input)) {
|
|
++input;
|
|
}
|
|
|
|
if (input == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
const char *sep[] = {"-", ":"};
|
|
int pythonic = (strchr(input, ':') || input[0] == '-') ? 1 : 0;
|
|
int index = 0;
|
|
|
|
for (char *token = strsep(&input, sep[pythonic]); token != NULL;
|
|
token = strsep(&input, sep[pythonic])) {
|
|
if (index == 0) {
|
|
item->start = item->stop = (int)strtol(token, NULL, 10);
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (token[0] == '\0') {
|
|
item->stop = -1;
|
|
continue;
|
|
}
|
|
|
|
item->stop = (int)strtol(token, NULL, 10);
|
|
if (pythonic) {
|
|
--item->stop;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
void parse_ranges(char *input, rofi_range_pair **list, unsigned int *length) {
|
|
char *endp;
|
|
if (input == NULL) {
|
|
return;
|
|
}
|
|
const char *const sep = ",";
|
|
for (char *token = strtok_r(input, sep, &endp); token != NULL;
|
|
token = strtok_r(NULL, sep, &endp)) {
|
|
// Make space.
|
|
*list =
|
|
g_realloc((*list), ((*length) + 1) * sizeof(struct rofi_range_pair));
|
|
// Parse a single pair.
|
|
if (parse_pair(token, &((*list)[*length]))) {
|
|
(*length)++;
|
|
}
|
|
}
|
|
}
|
|
void rofi_output_formatted_line(const char *format, const char *string,
|
|
int selected_line, const char *filter) {
|
|
for (int i = 0; format && format[i]; i++) {
|
|
if (format[i] == 'i') {
|
|
fprintf(stdout, "%d", selected_line);
|
|
} else if (format[i] == 'd') {
|
|
fprintf(stdout, "%d", (selected_line + 1));
|
|
} else if (format[i] == 's') {
|
|
fputs(string, stdout);
|
|
} else if (format[i] == 'p') {
|
|
char *esc = NULL;
|
|
pango_parse_markup(string, -1, 0, NULL, &esc, NULL, NULL);
|
|
if (esc) {
|
|
fputs(esc, stdout);
|
|
g_free(esc);
|
|
} else {
|
|
fputs("invalid string", stdout);
|
|
}
|
|
} else if (format[i] == 'q') {
|
|
char *quote = g_shell_quote(string);
|
|
fputs(quote, stdout);
|
|
g_free(quote);
|
|
} else if (format[i] == 'f') {
|
|
if (filter) {
|
|
fputs(filter, stdout);
|
|
}
|
|
} else if (format[i] == 'F') {
|
|
if (filter) {
|
|
char *quote = g_shell_quote(filter);
|
|
fputs(quote, stdout);
|
|
g_free(quote);
|
|
}
|
|
} else {
|
|
fputc(format[i], stdout);
|
|
}
|
|
}
|
|
fputc('\n', stdout);
|
|
fflush(stdout);
|
|
}
|
|
|
|
static gboolean helper_eval_cb2(const GMatchInfo *info, GString *res,
|
|
gpointer data) {
|
|
gchar *match;
|
|
// Get the match
|
|
int num_match = g_match_info_get_match_count(info);
|
|
// Just {text} This is inside () 5.
|
|
if (num_match == 5) {
|
|
match = g_match_info_fetch(info, 4);
|
|
if (match != NULL) {
|
|
// Lookup the match, so we can replace it.
|
|
gchar *r = g_hash_table_lookup((GHashTable *)data, match);
|
|
if (r != NULL) {
|
|
// Append the replacement to the string.
|
|
g_string_append(res, r);
|
|
}
|
|
// Free match.
|
|
g_free(match);
|
|
}
|
|
}
|
|
// {} with [] guard around it.
|
|
else if (num_match == 4) {
|
|
match = g_match_info_fetch(info, 2);
|
|
if (match != NULL) {
|
|
// Lookup the match, so we can replace it.
|
|
gchar *r = g_hash_table_lookup((GHashTable *)data, match);
|
|
if (r != NULL) {
|
|
// Add (optional) prefix
|
|
gchar *prefix = g_match_info_fetch(info, 1);
|
|
g_string_append(res, prefix);
|
|
g_free(prefix);
|
|
// Append the replacement to the string.
|
|
g_string_append(res, r);
|
|
// Add (optional) postfix
|
|
gchar *post = g_match_info_fetch(info, 3);
|
|
g_string_append(res, post);
|
|
g_free(post);
|
|
}
|
|
// Free match.
|
|
g_free(match);
|
|
}
|
|
}
|
|
// Else we have an invalid match.
|
|
// Continue replacement.
|
|
return FALSE;
|
|
}
|
|
|
|
char *helper_string_replace_if_exists(char *string, ...) {
|
|
GHashTable *h;
|
|
h = g_hash_table_new(g_str_hash, g_str_equal);
|
|
va_list ap;
|
|
va_start(ap, string);
|
|
// Add list from variable arguments.
|
|
while (1) {
|
|
char *key = va_arg(ap, char *);
|
|
if (key == (char *)0) {
|
|
break;
|
|
}
|
|
char *value = va_arg(ap, char *);
|
|
g_hash_table_insert(h, key, value);
|
|
}
|
|
char *retv = helper_string_replace_if_exists_v(string, h);
|
|
va_end(ap);
|
|
// Destroy key-value storage.
|
|
g_hash_table_destroy(h);
|
|
return retv;
|
|
}
|
|
/**
|
|
* @param string The string with elements to be replaced
|
|
* @param h Hash table with set of {key}, value that will be replaced,
|
|
* terminated by a NULL
|
|
*
|
|
* Items {key} are replaced by the value if '{key}' is passed as key/value
|
|
* pair, otherwise removed from string. If the {key} is in between [] all the
|
|
* text between [] are removed if {key} is not found. Otherwise key is
|
|
* replaced and [ & ] removed.
|
|
*
|
|
* This allows for optional replacement, f.e. '{ssh-client} [-t {title}] -e
|
|
* "{cmd}"' the '-t {title}' is only there if {title} is set.
|
|
*
|
|
* @returns a new string with the keys replaced.
|
|
*/
|
|
char *helper_string_replace_if_exists_v(char *string, GHashTable *h) {
|
|
GError *error = NULL;
|
|
char *res = NULL;
|
|
|
|
// Replace hits within {-\w+}.
|
|
GRegex *reg = g_regex_new("\\[(.*)({[-\\w]+})(.*)\\]|({[\\w-]+})",
|
|
G_REGEX_UNGREEDY, 0, &error);
|
|
if (error == NULL) {
|
|
res =
|
|
g_regex_replace_eval(reg, string, -1, 0, 0, helper_eval_cb2, h, &error);
|
|
}
|
|
// Free regex.
|
|
g_regex_unref(reg);
|
|
// Throw error if shell parsing fails.
|
|
if (error != NULL) {
|
|
char *msg = g_strdup_printf("Failed to parse: '%s'\nError: '%s'", string,
|
|
error->message);
|
|
rofi_view_error_dialog(msg, FALSE);
|
|
g_free(msg);
|
|
// print error.
|
|
g_error_free(error);
|
|
g_free(res);
|
|
return NULL;
|
|
}
|
|
return res;
|
|
}
|