diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 911d91b74..17f8420e4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -52,6 +52,6 @@ jobs: cc: gcc - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/Makefile.am b/Makefile.am index 3471bb560..a325f8991 100644 --- a/Makefile.am +++ b/Makefile.am @@ -182,6 +182,7 @@ if FOUND_PANDOC generate-manpage: doc/rofi.1\ doc/rofi-sensible-terminal.1\ doc/rofi-theme-selector.1\ + doc/rofi-actions.5\ doc/rofi-debugging.5\ doc/rofi-dmenu.5\ doc/rofi-keys.5\ diff --git a/config/config.c b/config/config.c index cf627dc7c..f0e7ba440 100644 --- a/config/config.c +++ b/config/config.c @@ -49,6 +49,18 @@ Settings config = { /** Custom command to generate preview icons */ .preview_cmd = NULL, + /** Custom command to call when menu selection changes */ + .on_selection_changed = NULL, + /** Custom command to call when menu mode changes */ + .on_mode_changed = NULL, + /** Custom command to call when menu entry is accepted */ + .on_entry_accepted = NULL, + /** Custom command to call when menu is canceled */ + .on_menu_canceled = NULL, + /** Custom command to call when menu finds errors */ + .on_menu_error = NULL, + /** Custom command to call when menu screenshot is taken */ + .on_screenshot_taken = NULL, /** Terminal to use. (for ssh and open in terminal) */ .terminal_emulator = "rofi-sensible-terminal", .ssh_client = "ssh", @@ -92,6 +104,8 @@ Settings config = { .sorting_method = "normal", /** Case sensitivity of the search */ .case_sensitive = FALSE, + /** Case smart of the search */ + .case_smart = FALSE, /** Cycle through in the element list */ .cycle = TRUE, /** Height of an element in #chars */ diff --git a/configure.ac b/configure.ac index e6e4d26a4..f5b9f4e04 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -AC_INIT([rofi], [1.7.8], [https://github.com/davatorium/rofi/],[],[https://github.com/davatorium/rofi/discussions]) +AC_INIT([rofi], [1.7.8-dev], [https://github.com/davatorium/rofi/],[],[https://github.com/davatorium/rofi/discussions]) AC_CONFIG_SRCDIR([source/rofi.c]) AC_CONFIG_HEADER([config.h]) diff --git a/doc/default_theme.rasi b/doc/default_theme.rasi index e2ffd351a..c18a84fe7 100644 --- a/doc/default_theme.rasi +++ b/doc/default_theme.rasi @@ -141,10 +141,17 @@ textbox-num-sep { str: "/"; } inputbar { - padding: 1px ; - spacing: 0px ; + padding: 1px; + spacing: 0px; text-color: var(normal-foreground); - children: [ prompt,textbox-prompt-colon,entry, num-filtered-rows, textbox-num-sep, num-rows, case-indicator ]; + children: [ prompt,textbox-prompt-colon,entry, overlay,num-filtered-rows, textbox-num-sep, num-rows, case-indicator ]; +} +overlay { + background-color: var(normal-foreground); + foreground-color: var(normal-background); + text-color: var(normal-background); + padding: 0 0.2em; + margin: 0 0.2em; } case-indicator { spacing: 0; diff --git a/doc/meson.build b/doc/meson.build index 7c399eb4c..5218d1a61 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -2,6 +2,7 @@ man_files = [ 'rofi.1', 'rofi-sensible-terminal.1', 'rofi-theme-selector.1', + 'rofi-actions.5', 'rofi-debugging.5', 'rofi-dmenu.5', 'rofi-keys.5', diff --git a/doc/rofi-actions.5.markdown b/doc/rofi-actions.5.markdown new file mode 100644 index 000000000..01f42f01b --- /dev/null +++ b/doc/rofi-actions.5.markdown @@ -0,0 +1,89 @@ +# rofi-actions(5) + +## NAME + +**rofi-actions** - Custom commands following interaction with rofi menus + +## DESCRIPTION + +**rofi** allows to set custom commands or scripts to be executed when some actions are performed in the menu, such as changing selection, accepting an entry or canceling. + +This makes it possible for example to play sound effects or read aloud menu entries on selection. + +## USAGE + +Following is the list of rofi flags for specifying custom commands or scripts to execute on supported actions: + +`-on-selection-changed` *cmd* + +Command or script to run when the current selection changes. Selected text is forwarded to the command replacing the pattern *{entry}*. + +`-on-entry-accepted` *cmd* + +Command or script to run when a menu entry is accepted. Accepted text is forwarded to the command replacing the pattern *{entry}*. + +`-on-mode-changed` *cmd* + +Command or script to run when the menu mode (e.g. drun,window,ssh...) is changed. + +`-on-menu-canceled` *cmd* + +Command or script to run when the menu is canceled. + +`-on-menu-error` *cmd* + +Command or script to run when an error menu is shown (e.g. `rofi -e "error message"`). Error text is forwarded to the command replacing the pattern *{error}*. + +`-on-screenshot-taken` *cmd* + +Command or script to run when a screenshot of rofi is taken. Screenshot path is forwarded to the command replacing the pattern *{path}*. + +### Example usage + +Rofi command line: + +```bash +rofi -on-selection-changed "/path/to/select.sh {entry}" \ + -on-entry-accepted "/path/to/accept.sh {entry}" \ + -on-menu-canceled "/path/to/exit.sh" \ + -on-mode-changed "/path/to/change.sh" \ + -on-menu-error "/path/to/error.sh {error}" \ + -on-screenshot-taken "/path/to/camera.sh {path}" \ + -show drun +``` + +Rofi config file: + +```css +configuration { + on-selection-changed: "/path/to/select.sh {entry}"; + on-entry-accepted: "/path/to/accept.sh {entry}"; + on-menu-canceled: "/path/to/exit.sh"; + on-mode-changed: "/path/to/change.sh"; + on-menu-error: "/path/to/error.sh {error}"; + on-screenshot-taken: "/path/to/camera.sh {path}"; +} +``` + +### Play sound effects + +Here's an example bash script that plays a sound effect using `aplay` when the current selection is changed: + +```bash +#!/bin/bash + +coproc aplay -q $HOME/Music/selecting_an_item.wav +``` + +The use of `coproc` for playing sounds is suggested, otherwise the rofi process will wait for sounds to end playback before exiting. + +### Read aloud + +Here's an example bash script that reads aloud currently selected entries using `espeak`: + +```bash +#!/bin/bash + +killall espeak +echo "selected: $@" | espeak +``` diff --git a/doc/rofi-dmenu.5.markdown b/doc/rofi-dmenu.5.markdown index 27e484b3f..0a272c17a 100644 --- a/doc/rofi-dmenu.5.markdown +++ b/doc/rofi-dmenu.5.markdown @@ -159,7 +159,7 @@ Hide the input text. This should not be considered secure! `-markup-rows` Tell **rofi** that DMenu input is Pango markup encoded, and should be rendered. -See [here](https://developer.gnome.org/pygtk/stable/pango-markup-language.html) +See [here](https://docs.gtk.org/Pango/pango_markup.html) for details about Pango markup. `-multi-select` diff --git a/doc/rofi-keys.5.markdown b/doc/rofi-keys.5.markdown index 8aeefacc2..d664958ac 100644 --- a/doc/rofi-keys.5.markdown +++ b/doc/rofi-keys.5.markdown @@ -495,6 +495,18 @@ Go down in the entry history. Default: Control+Down +`kb-matcher-up` + +Select the next matcher. + +Default: Super+equal + +`kb-matcher-down` + +Select the previous matcher. + +Default: Super+minus + ## Mouse Bindings `ml-row-left` diff --git a/doc/rofi-script.5.markdown b/doc/rofi-script.5.markdown index aeeac8fda..9d5d41c38 100644 --- a/doc/rofi-script.5.markdown +++ b/doc/rofi-script.5.markdown @@ -59,6 +59,7 @@ An integer number with the current state: - **0**: Initial call of script. - **1**: Selected an entry. - **2**: Selected a custom entry. +- **3**: Deleted an entry. - **10-28**: Custom keybinding 1-19 ( need to be explicitly enabled by script ). ### `ROFI_INFO` @@ -109,7 +110,7 @@ The following extra options exists: - **keep-selection**: If set, the selection is not moved to the first entry, but the current position is maintained. The filter is cleared. -- **keep-filter**: If set, the filter is not cleared. +- **keep-filter**: If set, the filter is not cleared. - **new-selection**: If `keep-selection` is set, this allows you to override the selected entry (absolute position). diff --git a/doc/rofi-theme.5.markdown b/doc/rofi-theme.5.markdown index dd6c2b4f1..c03810356 100644 --- a/doc/rofi-theme.5.markdown +++ b/doc/rofi-theme.5.markdown @@ -1118,6 +1118,10 @@ The following properties are currently supported: - **require-input**: boolean Listview requires user input to be unhidden. The list is still present and hitting accept will activate the first entry. +### Overlay widget + +- **timeout**: The time the widget is visible when showing a temporary message. + ## Listview widget The listview widget is special container widget. @@ -1658,6 +1662,14 @@ If a filename is provided, it will try to resolve it in the following order: A name is resolved (if it has no valid extension) as a filename by appending the `.rasi` and the `.rasinc` extension. It will first look for files with `.rasi`, then for files with `.rasinc`. +If you want to do an optional import, e.g. no error when the file does not exists, you can do: + +```css +?import "myfile" +``` + +This still throws an error on syntax error, but won't abort parsing if file does not exists. + ## Examples Several examples are installed together with **rofi**. These can be found in diff --git a/doc/rofi.1.markdown b/doc/rofi.1.markdown index 28638516f..60e84c079 100644 --- a/doc/rofi.1.markdown +++ b/doc/rofi.1.markdown @@ -246,6 +246,12 @@ exec command. For that case, `#` can be used as a separator. Start in case-sensitive mode. This option can be changed at run-time using the `-kb-toggle-case-sensitivity` key binding. +`-case-smart` + +Start in case-smart mode behave like vim's `smartcase`, which determines +case-sensitivity by input. When enabled, this will suppress `-case-sensitive` +config. + `-cycle` Cycle through the result list. Default is 'true'. @@ -355,6 +361,9 @@ Currently, the following methods are supported: Default: *normal* +Multiple matching methods can be specified in a comma separated list. +The matching up/down keybinding allows cycling through at runtime. + Note: glob matching might be slow for larger lists `-tokenize` diff --git a/include/helper.h b/include/helper.h index d85e4737b..c16daeb02 100644 --- a/include/helper.h +++ b/include/helper.h @@ -200,13 +200,15 @@ char *rofi_expand_path(const char *input); * @param needlelen The length of the needle * @param haystack The string to match against * @param haystacklen The length of the haystack + * @param case_sensitive Whether case is significant. * * UTF-8 aware levenshtein distance calculation * * @returns the levenshtein distance between needle and haystack */ unsigned int levenshtein(const char *needle, const glong needlelen, - const char *haystack, const glong haystacklen); + const char *haystack, const glong haystacklen, + const int case_sensitive); /** * @param data the unvalidated character array holding possible UTF-8 data @@ -234,6 +236,7 @@ char *rofi_latin_to_utf8_strdup(const char *input, gssize length); * @param plen Pattern length. * @param str The input to match against pattern. * @param slen Length of str. + * @param case_sensitive Whether case is significant. * * rofi_scorer_fuzzy_evaluate implements a global sequence alignment algorithm * to find the maximum accumulated score by aligning `pattern` to `str`. It @@ -263,7 +266,7 @@ char *rofi_latin_to_utf8_strdup(const char *input, gssize length); * @returns the sorting weight. */ int rofi_scorer_fuzzy_evaluate(const char *pattern, glong plen, const char *str, - glong slen); + glong slen, const int case_sensitive); /*@}*/ /** @@ -353,6 +356,13 @@ cairo_surface_t *cairo_image_surface_create_from_svg(const gchar *file, */ void parse_ranges(char *input, rofi_range_pair **list, unsigned int *length); +/** + * @param input String to parse + * + * @returns String matching should be case sensitive or insensitive + */ +int parse_case_sensitivity(const char *input); + /** * @param format The format string used. See below for possible syntax. * @param string The selected entry. @@ -432,6 +442,19 @@ ConfigEntry *rofi_config_find_widget(const char *name, const char *state, */ Property *rofi_theme_find_property(ConfigEntry *widget, PropertyType type, const char *property, gboolean exact); + +/** + * @returns get a human readable string with the current matching method. + */ +const char *helper_get_matching_mode_str(void); +/** + * Switch to the next matching method. + */ +void helper_select_next_matching_mode(void); +/** + * Switch to the previous matching method. + */ +void helper_select_previous_matching_mode(void); G_END_DECLS /**@} */ diff --git a/include/keyb.h b/include/keyb.h index 850df0e6a..33c3f39f4 100644 --- a/include/keyb.h +++ b/include/keyb.h @@ -145,6 +145,8 @@ typedef enum { SELECT_ELEMENT_10, ENTRY_HISTORY_UP, ENTRY_HISTORY_DOWN, + MATCHER_UP, + MATCHER_DOWN } KeyBindingAction; /** diff --git a/include/mode-private.h b/include/mode-private.h index 56d4623df..a3b001da2 100644 --- a/include/mode-private.h +++ b/include/mode-private.h @@ -31,9 +31,6 @@ #include G_BEGIN_DECLS -/** ABI version to check if loaded plugin is compatible. */ -#define ABI_VERSION 7u - /** * Indicator what type of mode this is. * For now it can be the classic switcher, or also implement a completer. diff --git a/include/mode.h b/include/mode.h index 55ef750d9..7f568d81a 100644 --- a/include/mode.h +++ b/include/mode.h @@ -29,7 +29,12 @@ #define ROFI_MODE_H #include "rofi-types.h" #include +#include G_BEGIN_DECLS + +/** ABI version to check if loaded plugin is compatible. */ +#define ABI_VERSION 7u + /** * @defgroup MODE Mode * @@ -277,6 +282,28 @@ ModeMode mode_completer_result(Mode *sw, int menu_retv, char **input, * @returns TRUE if mode can be used as completer. */ gboolean mode_is_completer(const Mode *sw); + +/** + * @param mode The mode to query + * + * @returns the modes ABI version. + */ +int mode_get_abi_version(Mode *const mode); + +/** + * @param mode The mode to query + * @param mod The GModule used to load the mode + * + * Set GModule used to load this plugin, this is used to + * unload it on shutdown. + */ +void mode_plugin_set_module(Mode *mode, GModule *mod); +/** + * @param mode The mode to query + * + * @returns the GModule used to load this plugin. NULL if not a plugin. + */ +GModule *mode_plugin_get_module(Mode *mode); /**@}*/ G_END_DECLS #endif diff --git a/include/settings.h b/include/settings.h index f9853f20b..03c8eaba5 100644 --- a/include/settings.h +++ b/include/settings.h @@ -40,7 +40,8 @@ typedef enum { MM_REGEX = 1, MM_GLOB = 2, MM_FUZZY = 3, - MM_PREFIX = 4 + MM_PREFIX = 4, + MM_NUM_MATCHERS = 5 } MatchingMethod; /** @@ -69,6 +70,18 @@ typedef struct { /** Custom command to generate preview icons */ char *preview_cmd; + /** Custom command to call when menu selection changes */ + char *on_selection_changed; + /** Custom command to call when menu mode changes */ + char *on_mode_changed; + /** Custom command to call when menu entry is accepted */ + char *on_entry_accepted; + /** Custom command to call when menu is canceled */ + char *on_menu_canceled; + /** Custom command to call when menu finds errors */ + char *on_menu_error; + /** Custom command to call when menu screenshot is taken */ + char *on_screenshot_taken; /** Terminal to use */ char *terminal_emulator; /** SSH client to use */ @@ -123,6 +136,8 @@ typedef struct { /** Search case sensitivity */ unsigned int case_sensitive; + /** Smart case sensitivity like vim */ + unsigned int case_smart; /** Cycle through in the element list */ unsigned int cycle; /** Height of an element in number of rows */ diff --git a/include/view-internal.h b/include/view-internal.h index 754ae004a..f94723aa7 100644 --- a/include/view-internal.h +++ b/include/view-internal.h @@ -101,6 +101,8 @@ struct RofiViewState { int skip_absorb; /** The selected line (in the unfiltered list) */ unsigned int selected_line; + /** The previously selected line (in the unfiltered list) */ + unsigned int previous_line; /** The return state of the view */ MenuReturn retv; /** Monitor #workarea the view is displayed on */ @@ -159,12 +161,13 @@ struct RofiViewState { /** Regexs used for matching */ rofi_int_matcher **tokens; + /** For case-sensitivity */ + gboolean case_sensitive; }; /** @} */ typedef struct _view_proxy { void (*update)(struct RofiViewState *state, gboolean qr); - void (*maybe_update)(struct RofiViewState *state); void (*temp_configure_notify)(struct RofiViewState *state, xcb_configure_notify_event_t *xce); void (*temp_click_to_exit)(struct RofiViewState *state, xcb_window_t target); @@ -222,6 +225,8 @@ struct _rofi_view_cache_state { gboolean delayed_mode; /** timeout handling */ guint user_timeout; + /** timeout overlay */ + guint overlay_timeout; /** Entry box */ gboolean entry_history_enable; /** Array with history entriy input. */ diff --git a/include/view.h b/include/view.h index e6c8a204b..37541c88c 100644 --- a/include/view.h +++ b/include/view.h @@ -287,6 +287,14 @@ void rofi_view_switch_mode(RofiViewState *state, Mode *mode); * Overlays text over the current view. Passing NULL for text hides the overlay. */ void rofi_view_set_overlay(RofiViewState *state, const char *text); +/** + * @param state The handle to the view + * @param text An UTF-8 encoded character array with the text to overlay. + * + * Overlays text over the current view. Passing NULL for text hides the overlay. + * This message is automatically removed after X seconds. + */ +void rofi_view_set_overlay_timeout (RofiViewState *state, const char *text); /** * @param state The handle to the view. diff --git a/include/widgets/textbox.h b/include/widgets/textbox.h index 8b120dc74..574b2ade9 100644 --- a/include/widgets/textbox.h +++ b/include/widgets/textbox.h @@ -80,7 +80,9 @@ typedef struct { TBFontConfig *tbfc; PangoEllipsizeMode emode; - // + + const char *password_mask_char; + const char *theme_name; } textbox; diff --git a/lexer/theme-lexer.l b/lexer/theme-lexer.l index 5af651ca2..bf5b5bd4b 100644 --- a/lexer/theme-lexer.l +++ b/lexer/theme-lexer.l @@ -51,6 +51,8 @@ int last_state = 0; extern int rofi_is_in_dmenu_mode; +gboolean import_optional = FALSE; + const char *rasi_theme_file_extensions[] = {".rasi", ".rasinc", NULL}; /** * Type of Object to parse. @@ -289,6 +291,7 @@ C_COMMENT_OPEN "/*" INCLUDE "@import" +OPT_INCLUDE "?import" THEME "@theme" DEFAULT (?i:\"default\"?) @@ -378,6 +381,12 @@ if ( queue == NULL ) { */ {INCLUDE} { g_queue_push_head ( queue, GINT_TO_POINTER (YY_START) ); + import_optional = FALSE; + BEGIN(INCLUDE); +} +{OPT_INCLUDE} { + g_queue_push_head ( queue, GINT_TO_POINTER (YY_START) ); + import_optional = TRUE; BEGIN(INCLUDE); } {THEME} { @@ -437,10 +446,15 @@ if ( queue == NULL ) { yylloc->first_column = yylloc->last_column = 1; yylloc->filename = current->filename; } else { - char *str = g_markup_printf_escaped ( "Failed to open theme: %s\nError: %s", - filename, strerror ( errno ) ); - rofi_add_warning_message ( g_string_new ( str ) ); - g_free ( str ); + if ( !import_optional ) { + char *str = g_markup_printf_escaped ( "Failed to open theme: %s\nError: %s", + filename, strerror ( errno ) ); + rofi_add_warning_message ( g_string_new ( str ) ); + g_free ( str ); + } else { + g_warning("Trying to parse optional theme: '%s', Error: %s", + filename, strerror(errno)); + } g_free(filename); } // Pop out of include. */ @@ -844,7 +858,7 @@ if ( queue == NULL ) { /** * Media defaults. */ -{WHITESPACE}+ ; // ignore all whitespace +{WHITESPACE}+ ; // ignore all whitespace . { yytext[yyleng-1] = '\0'; diff --git a/meson.build b/meson.build index 55b55f65f..b9e416128 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('rofi', 'c', - version: '1.7.8+wayland1', + version: '1.7.8+wayland1-dev', meson_version: '>=0.59.0', license: [ 'MIT' ], default_options: [ diff --git a/mkdocs/docs/themes/capture.sh b/mkdocs/docs/themes/capture.sh index bb0be6bb3..3506047cf 100755 --- a/mkdocs/docs/themes/capture.sh +++ b/mkdocs/docs/themes/capture.sh @@ -3,55 +3,49 @@ THEMES=../../../themes/*.rasi ROFI_BIN=../../../build/rofi - -function generate_options() -{ - echo -en "rofi\0icon\x1frofi\n" - echo -en "help browser\0icon\x1fhelp-browser\n" - echo -en "thunderbird\0icon\x1fthunderbird\n" - echo -en "Urgent\0icon\x1femblem-urgent\n" - echo -en "Active\0icon\x1fface-wink\n" - echo -en "folder\0icon\x1ffolder\n" - echo -en "Icon font 🐢 🥳\n" - echo -en "Font icon\0icon\x1f:-)\n" - echo -en "Quit\0icon\x1fapplication-exit\n" +function generate_options() { + echo -en "rofi\0icon\x1frofi\n" + echo -en "help browser\0icon\x1fhelp-browser\n" + echo -en "thunderbird\0icon\x1fthunderbird\n" + echo -en "Urgent\0icon\x1femblem-urgent\n" + echo -en "Active\0icon\x1fface-wink\n" + echo -en "folder\0icon\x1ffolder\n" + echo -en "Icon font 🐢 🥳\n" + echo -en "Font icon\0icon\x1f:-)\n" + echo -en "Quit\0icon\x1fapplication-exit\n" } -function run_theme -{ - theme=$1 - BASE=$(basename ${theme}) - NAME=${BASE%.rasi} - export ROFI_PNG_OUTPUT="${NAME}.png" - if [ ${NAME} = "default" ] - then - echo "# Default theme" >> themes.md - else - echo "# [${NAME}](https://github.com/davatorium/rofi/blob/next/themes/${BASE})" >> themes.md - fi - echo "" >> themes.md - generate_options | ${ROFI_BIN} -theme-str "@theme \"${theme}\"" \ - -no-config -dmenu -p "mode" -show-icons \ - -u 3 -a 4 -mesg "Message box for extra information" \ - -take-screenshot-quit 1500 - - echo "![${NAME}](${NAME}.png)" >> themes.md - echo "" >> themes.md +function run_theme { + theme=$1 + BASE="$(basename ${theme})" + NAME=${BASE%.rasi} + export ROFI_PNG_OUTPUT="${NAME}.png" + if [ "${NAME}" = "default" ]; then + echo "# Default theme" >>themes.md + else + echo "# [${NAME}](https://github.com/davatorium/rofi/blob/next/themes/${BASE})" >>themes.md + fi + echo "" >>themes.md + generate_options | ${ROFI_BIN} -theme-str "@theme \"${theme}\"" \ + -no-config -dmenu -p "mode" -show-icons \ + -u 3 -a 4 -mesg "Message box for extra information" \ + -take-screenshot-quit 1500 + + echo "![${NAME}](${NAME}.png)" >>themes.md + echo "" >>themes.md } -echo "# Included Themes" > themes.md - -echo "Below is a list of themes shipped with rofi." >> themes.md -echo "Use \`rofi-theme-selector\` to select and use one of these themes." >> themes.md +echo "# Included Themes" >themes.md +echo "Below is a list of themes shipped with rofi." >>themes.md +echo "Use \`rofi-theme-selector\` to select and use one of these themes." >>themes.md Xvfb :1234 -screen 0 1920x1080x24 & XEPHYR_PID=$! export DISPLAY=:1234 -sleep 0.5; +sleep 0.5 run_theme "default" -for theme in ${THEMES} -do - run_theme ${theme} +for theme in ${THEMES}; do + run_theme "${theme}" done kill ${XEPHYR_PID} diff --git a/source/helper.c b/source/helper.c index b4cd9b606..6265f6b6c 100644 --- a/source/helper.c +++ b/source/helper.c @@ -54,6 +54,16 @@ #include #include +const char *const MatchingMethodStr[MM_NUM_MATCHERS] = { + "Normal", "Regex", "Glob", "Fuzzy", "Prefix"}; + +static int MatchingMethodEnabled[MM_NUM_MATCHERS] = { + MM_NORMAL, + -1, +}; +static int NUMMatchingMethodEnabled = 1; +static int CurrentMatchingMethod = 0; + /** * Textual description of positioning rofi. */ @@ -67,6 +77,23 @@ char **stored_argv = NULL; char *helper_string_replace_if_exists_v(char *string, GHashTable *h); +const char *helper_get_matching_mode_str(void) { + return MatchingMethodStr[config.matching_method]; +} +void helper_select_next_matching_mode(void) { + + CurrentMatchingMethod++; + CurrentMatchingMethod %= NUMMatchingMethodEnabled; + config.matching_method = MatchingMethodEnabled[CurrentMatchingMethod]; +} +void helper_select_previous_matching_mode(void) { + CurrentMatchingMethod--; + if (CurrentMatchingMethod < 0) { + CurrentMatchingMethod = NUMMatchingMethodEnabled - 1; + } + config.matching_method = MatchingMethodEnabled[CurrentMatchingMethod]; +} + void cmd_set_arguments(int argc, char **argv) { stored_argc = argc; stored_argv = argv; @@ -582,7 +609,7 @@ int create_pid_file(const char *pidfile, gboolean kill_running) { char buffer[64] = { 0, }; - ssize_t l = read(fd, &buffer, 63); + ssize_t l = read(fd, &(buffer[0]), 63); if (l > 1) { buffer[l] = 0; pid_t pid = g_ascii_strtoll(buffer, NULL, 0); @@ -664,24 +691,40 @@ int config_sanity_check(void) { } 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, - "\tconfig.matching=%s is not a valid " - "matching strategy.\nValid options are: glob, " - "regex, fuzzy, prefix or normal.\n", - config.matching); - found_error = 1; + char **strv = g_strsplit(config.matching, ",", 0); + if (strv) { + int matching_method_index = 0; + for (char **str = strv; *str && matching_method_index < MM_NUM_MATCHERS; + str++) { + gboolean found = FALSE; + for (unsigned i = 0; + i < MM_NUM_MATCHERS && matching_method_index < MM_NUM_MATCHERS; + i++) { + if (g_ascii_strcasecmp(*str, MatchingMethodStr[i]) == 0) { + MatchingMethodEnabled[matching_method_index] = i; + matching_method_index++; + NUMMatchingMethodEnabled = matching_method_index; + if (matching_method_index == MM_NUM_MATCHERS) { + found_error = 1; + g_string_append_printf(msg, + "\tconfig.matching = %s to many " + "matching options enabled.\n", + config.matching); + } + found = TRUE; + } + } + if (!found) { + g_string_append_printf(msg, + "\tconfig.matching=%s is not a valid " + "matching strategy.\nValid options are: glob, " + "regex, fuzzy, prefix or normal.\n", + *str); + found_error = 1; + } + } + config.matching_method = MatchingMethodEnabled[0]; + g_strfreev(strv); } } @@ -769,7 +812,8 @@ char *rofi_expand_path(const char *input) { ((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) { + const char *haystack, const glong haystacklen, + int case_sensitive) { if (needlelen == G_MAXLONG) { // String to long, we cannot handle this. return UINT_MAX; @@ -785,12 +829,12 @@ unsigned int levenshtein(const char *needle, const glong needlelen, const char *needles = needle; column[0] = x; gunichar haystackc = g_utf8_get_char(haystack); - if (!config.case_sensitive) { + if (!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) { + if (!case_sensitive) { needlec = g_unichar_tolower(needlec); } unsigned int olddiag = column[y]; @@ -917,7 +961,7 @@ static int rofi_scorer_get_score_for(enum CharClass prev, enum CharClass curr) { } int rofi_scorer_fuzzy_evaluate(const char *pattern, glong plen, const char *str, - glong slen) { + glong slen, int case_sensitive) { if (slen > FUZZY_SCORER_MAX_LENGTH) { return -MIN_SCORE; } @@ -952,9 +996,8 @@ int rofi_scorer_fuzzy_evaluate(const char *pattern, glong plen, const char *str, 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)) { + if (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 @@ -1248,6 +1291,28 @@ void parse_ranges(char *input, rofi_range_pair **list, unsigned int *length) { } } } + +int parse_case_sensitivity(const char *input) { + int case_sensitive = config.case_sensitive; + if (config.case_smart) { + // By default case is false, unless the search query has a + // uppercase in it? + case_sensitive = FALSE; + const char *end; + if (g_utf8_validate(input, -1, &end)) { + for (const char *c = (input); !case_sensitive && c != NULL && *c; + c = g_utf8_next_char(c)) { + gunichar uc = g_utf8_get_char(c); + if (g_unichar_isupper(uc)) { + case_sensitive = TRUE; + } + } + } + } + + return case_sensitive; +} + 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++) { diff --git a/source/keyb.c b/source/keyb.c index 9de5829b9..c1bb2229b 100644 --- a/source/keyb.c +++ b/source/keyb.c @@ -332,6 +332,14 @@ ActionBindingEntry rofi_bindings[] = { .name = "kb-entry-history-down", .binding = "Control+Down", .comment = "Go down in the history of the entry box"}, + {.id = MATCHER_UP, + .name = "kb-matcher-up", + .binding = "Super+equal", + .comment = "Switch to the previous matcher"}, + {.id = MATCHER_DOWN, + .name = "kb-mather-down", + .binding = "Super+minus", + .comment = "Switch to the next matcher"}, /* Mouse-aware bindings */ diff --git a/source/mode.c b/source/mode.c index 7cb070deb..64d3994b8 100644 --- a/source/mode.c +++ b/source/mode.c @@ -159,6 +159,11 @@ const char *mode_get_name(const Mode *mode) { return mode->name; } +int mode_get_abi_version(Mode *const mode) { + g_assert(mode != NULL); + return mode->abi_version; +} + void mode_free(Mode **mode) { g_assert(mode != NULL); g_assert((*mode) != NULL); @@ -245,4 +250,10 @@ gboolean mode_is_completer(const Mode *mode) { return FALSE; } +void mode_plugin_set_module(Mode *mode, GModule *mod){ + mode->module = mod; +} +GModule *mode_plugin_get_module(Mode *mode){ + return mode->module; +} /**@}*/ diff --git a/source/modes/dmenu.c b/source/modes/dmenu.c index 31c3de4d7..65b1ef2fa 100644 --- a/source/modes/dmenu.c +++ b/source/modes/dmenu.c @@ -289,8 +289,8 @@ static gpointer read_input_thread(gpointer userdata) { if (FD_ISSET(fd, &rfds)) { ssize_t readbytes = 0; if ((nread + 1024) > len) { - line = g_realloc(line, (nread + 1024)); - len = nread + 1024; + line = g_realloc(line, (len + 2048)); + len = len + 2048; } readbytes = read(fd, &line[nread], 1023); if (readbytes > 0) { @@ -959,7 +959,8 @@ int dmenu_mode_dialog(void) { char *select = NULL; find_arg_str("-select", &select); if (select != NULL) { - rofi_int_matcher **tokens = helper_tokenize(select, config.case_sensitive); + rofi_int_matcher **tokens = + helper_tokenize(select, parse_case_sensitivity(select)); unsigned int i = 0; for (i = 0; i < cmd_list_length; i++) { if (helper_token_match(tokens, cmd_list[i].entry)) { @@ -970,8 +971,9 @@ int dmenu_mode_dialog(void) { helper_tokenize_free(tokens); } if (find_arg("-dump") >= 0) { - rofi_int_matcher **tokens = helper_tokenize( - config.filter ? config.filter : "", config.case_sensitive); + char *filter = config.filter ? config.filter : ""; + rofi_int_matcher **tokens = + helper_tokenize(filter, parse_case_sensitivity(filter)); unsigned int i = 0; for (i = 0; i < cmd_list_length; i++) { if (tokens == NULL || helper_token_match(tokens, cmd_list[i].entry)) { diff --git a/source/modes/recursivebrowser.c b/source/modes/recursivebrowser.c index a22896b5b..d108bd17e 100644 --- a/source/modes/recursivebrowser.c +++ b/source/modes/recursivebrowser.c @@ -353,7 +353,8 @@ static gboolean recursive_browser_async_read_proc(gint fd, } } else if (command == 'q') { if (pd->loading) { - rofi_view_set_overlay(rofi_view_get_active(), NULL); + // TODO: add enable. + //rofi_view_set_overlay(rofi_view_get_active(), NULL); } } } diff --git a/source/modes/script.c b/source/modes/script.c index 282f0d5c9..1db6d8780 100644 --- a/source/modes/script.c +++ b/source/modes/script.c @@ -350,6 +350,10 @@ static ModeMode script_mode_result(Mode *sw, int mretv, char **input, retv = (mretv & MENU_LOWER_MASK); return retv; } + } else if ((mretv & MENU_ENTRY_DELETE) && selected_line != UINT32_MAX) { + script_mode_reset_highlight(sw); + new_list = execute_executor(sw, rmpd->cmd_list[selected_line].entry, &new_length, + 3, &(rmpd->cmd_list[selected_line])); } else if ((mretv & MENU_OK) && rmpd->cmd_list[selected_line].entry != NULL) { if (rmpd->cmd_list[selected_line].nonselectable) { return RELOAD_DIALOG; diff --git a/source/rofi.c b/source/rofi.c index 44004de3f..62134032b 100644 --- a/source/rofi.c +++ b/source/rofi.c @@ -71,10 +71,6 @@ #include "timings.h" -// Plugin abi version. -// TODO: move this check to mode.c -#include "mode-private.h" - /** Location of pidfile for this instance. */ char *pidfile = NULL; /** Location of Cache directory. */ @@ -202,7 +198,7 @@ static void run_mode_index(ModeMode mode) { for (unsigned int i = 0; i < num_modes; i++) { if (!mode_init(modes[i])) { GString *str = g_string_new("Failed to initialize the mode: "); - g_string_append(str, modes[i]->name); + g_string_append(str, mode_get_name(modes[i])); g_string_append(str, "\n"); rofi_view_error_dialog(str->str, ERROR_MSG_MARKUP); @@ -303,7 +299,7 @@ static void print_list_of_modes(int is_term) { } printf(" • %s%s%s%s\n", active ? "+" : "", is_term ? (active ? color_green : color_red) : "", - available_modes[i]->name, is_term ? color_reset : ""); + mode_get_name(available_modes[i]), is_term ? color_reset : ""); } } static void print_main_application_options(int is_term) { @@ -475,7 +471,7 @@ static void help_print_mode_not_found(const char *mode) { } } g_string_append_printf(str, " * %s%s\n", active ? "+" : "", - available_modes[i]->name); + mode_get_name(available_modes[i])); } rofi_add_error_message(str); } @@ -489,7 +485,7 @@ static void help_print_no_arguments(void) { g_string_append(emesg, "The following modes are enabled:\n"); for (unsigned int j = 0; j < num_modes; j++) { g_string_append_printf(emesg, " • %s\n", - modes[j]->name); + mode_get_name(modes[j])); } g_string_append(emesg, "\nThe following modes can be enabled:\n"); for (unsigned int i = 0; i < num_available_modes; i++) { @@ -502,7 +498,7 @@ static void help_print_no_arguments(void) { } if (!active) { g_string_append_printf(emesg, " • %s\n", - available_modes[i]->name); + mode_get_name(available_modes[i])); } } g_string_append(emesg, "\nTo activate a mode, add it to the list in " @@ -565,7 +561,7 @@ static void cleanup(void) { Mode *rofi_collect_modes_search(const char *name) { for (unsigned int i = 0; i < num_available_modes; i++) { - if (g_strcmp0(name, available_modes[i]->name) == 0) { + if (g_strcmp0(name, mode_get_name(available_modes[i])) == 0) { return available_modes[i]; } } @@ -577,7 +573,7 @@ Mode *rofi_collect_modes_search(const char *name) { * @returns TRUE when success. */ static gboolean rofi_collectmodes_add(Mode *mode) { - Mode *m = rofi_collect_modes_search(mode->name); + Mode *m = rofi_collect_modes_search(mode_get_name(mode)); if (m == NULL) { available_modes = g_realloc(available_modes, sizeof(Mode *) * (num_available_modes + 1)); @@ -605,13 +601,13 @@ static void rofi_collectmodes_dir(const char *base_dir) { if (mod) { Mode *m = NULL; if (g_module_symbol(mod, "mode", (gpointer *)&m)) { - if (m->abi_version != ABI_VERSION) { + if (mode_get_abi_version(m) != ABI_VERSION) { g_warning("ABI version of plugin: '%s' does not match: %08X " "expecting: %08X", - dn, m->abi_version, ABI_VERSION); + dn, mode_get_abi_version(m), ABI_VERSION); g_module_close(mod); } else { - m->module = mod; + mode_plugin_set_module(m, mod); if (!rofi_collectmodes_add(m)) { g_module_close(mod); } @@ -684,8 +680,8 @@ static void rofi_collectmodes_setup(void) { } static void rofi_collectmodes_destroy(void) { for (unsigned int i = 0; i < num_available_modes; i++) { - if (available_modes[i]->module) { - GModule *mod = available_modes[i]->module; + if (mode_plugin_get_module(available_modes[i])) { + GModule *mod = mode_plugin_get_module(available_modes[i]); available_modes[i] = NULL; g_module_close(mod); } @@ -1107,7 +1103,9 @@ int main(int argc, char *argv[]) { extern const char *rasi_theme_file_extensions[]; char *file2 = helper_get_theme_path(config_path, rasi_theme_file_extensions, NULL); - char *filename = rofi_theme_parse_prepare_file(file2); + GFile *gf = g_file_new_for_path(file2); + char *filename = g_file_get_path(gf); + g_object_unref(gf); g_free(file2); if (filename && g_file_test(filename, G_FILE_TEST_EXISTS)) { if (rofi_theme_parse_file(filename)) { diff --git a/source/view.c b/source/view.c index 7b488edc5..7a51f5c26 100644 --- a/source/view.c +++ b/source/view.c @@ -81,14 +81,15 @@ struct _rofi_view_cache_state CacheState = { .max_refilter_time = 0.0, .delayed_mode = FALSE, .user_timeout = 0, + .overlay_timeout = 0, .entry_history_enable = TRUE, .entry_history = NULL, .entry_history_length = 0, .entry_history_index = 0, }; -static char *get_matching_state(void) { - if (config.case_sensitive) { +static char *get_matching_state(RofiViewState* state) { + if (state->case_sensitive) { if (config.sort) { return "±"; } else { @@ -113,6 +114,18 @@ static int lev_sort(const void *p1, const void *p2, void *arg) { return distances[*a] - distances[*b]; } +static void screenshot_taken_user_callback(const char *path) { + if (config.on_screenshot_taken == NULL) + return; + + char **args = NULL; + int argv = 0; + helper_parse_setup(config.on_screenshot_taken, &args, &argv, "{path}", path, + (char *)0); + if (args != NULL) + helper_execute(NULL, args, "", config.on_screenshot_taken, NULL); +} + /** * Stores a screenshot of Rofi at that point in time. */ @@ -172,6 +185,7 @@ void rofi_capture_screenshot(void) { g_warning("Failed to produce screenshot '%s', got error: '%s'", fpath, cairo_status_to_string(status)); } + screenshot_taken_user_callback(fpath); } cairo_destroy(draw); } @@ -444,12 +458,13 @@ static void filter_elements(thread_state *ts, glong slen = g_utf8_strlen(str, -1); switch (config.sorting_method_enum) { case SORT_FZF: - t->state->distance[i] = - rofi_scorer_fuzzy_evaluate(t->pattern, t->plen, str, slen); + t->state->distance[i] = rofi_scorer_fuzzy_evaluate( + t->pattern, t->plen, str, slen, t->state->case_sensitive); break; case SORT_NORMAL: default: - t->state->distance[i] = levenshtein(t->pattern, t->plen, str, slen); + t->state->distance[i] = levenshtein(t->pattern, t->plen, str, slen, + t->state->case_sensitive); break; } g_free(str); @@ -620,9 +635,31 @@ inline static void rofi_view_nav_last(RofiViewState *state) { // state->selected = state->filtered_lines - 1; listview_set_selected(state->list_view, -1); } +static void selection_changed_user_callback(unsigned int index, + RofiViewState *state) { + if (config.on_selection_changed == NULL) + return; + + int fstate = 0; + char *text = mode_get_display_value(state->sw, state->line_map[index], + &fstate, NULL, TRUE); + char **args = NULL; + int argv = 0; + helper_parse_setup(config.on_selection_changed, &args, &argv, "{entry}", text, + (char *)0); + if (args != NULL) + helper_execute(NULL, args, "", config.on_selection_changed, NULL); + g_free(text); +} static void selection_changed_callback(G_GNUC_UNUSED listview *lv, unsigned int index, void *udata) { RofiViewState *state = (RofiViewState *)udata; + if (index < state->filtered_lines) { + if (state->previous_line != state->line_map[index]) { + selection_changed_user_callback(index, state); + state->previous_line = state->line_map[index]; + } + } if (state->tb_current_entry) { if (index < state->filtered_lines) { int fstate = 0; @@ -630,7 +667,6 @@ static void selection_changed_callback(G_GNUC_UNUSED listview *lv, &fstate, NULL, TRUE); textbox_text(state->tb_current_entry, text); g_free(text); - } else { textbox_text(state->tb_current_entry, ""); } @@ -742,7 +778,12 @@ static gboolean rofi_view_refilter_real(RofiViewState *state) { unsigned int j = 0; gchar *pattern = mode_preprocess_input(state->sw, state->text->text); glong plen = pattern ? g_utf8_strlen(pattern, -1) : 0; - state->tokens = helper_tokenize(pattern, config.case_sensitive); + state->case_sensitive = parse_case_sensitivity(state->text->text); + state->tokens = helper_tokenize(pattern, state->case_sensitive); + + if (config.case_smart && state->case_indicator) { + textbox_text(state->case_indicator, get_matching_state(state)); + } /** * On long lists it can be beneficial to parallelize. * If number of threads is 1, no thread is spawn. @@ -1008,7 +1049,7 @@ static void rofi_view_trigger_global_action(KeyBindingAction action) { if (state->case_indicator != NULL) { config.sort = !config.sort; state->refilter = TRUE; - textbox_text(state->case_indicator, get_matching_state()); + textbox_text(state->case_indicator, get_matching_state(state)); } break; case MODE_PREVIOUS: @@ -1038,7 +1079,7 @@ static void rofi_view_trigger_global_action(KeyBindingAction action) { config.case_sensitive = !config.case_sensitive; (state->selected_line) = 0; state->refilter = TRUE; - textbox_text(state->case_indicator, get_matching_state()); + textbox_text(state->case_indicator, get_matching_state(state)); } break; // Special delete entry command. @@ -1214,7 +1255,6 @@ static void rofi_view_trigger_global_action(KeyBindingAction action) { // Nothing entered and nothing selected. state->retv = MENU_CUSTOM_INPUT; } - state->quit = TRUE; break; } @@ -1273,6 +1313,16 @@ static void rofi_view_trigger_global_action(KeyBindingAction action) { } break; } + case MATCHER_UP: + helper_select_next_matching_mode(); + rofi_view_refilter(state); + rofi_view_set_overlay_timeout(state, helper_get_matching_mode_str()); + break; + case MATCHER_DOWN: + helper_select_previous_matching_mode(); + rofi_view_refilter(state); + rofi_view_set_overlay_timeout(state, helper_get_matching_mode_str()); + break; } } @@ -1410,6 +1460,62 @@ void rofi_view_handle_mouse_motion(RofiViewState *state, gint x, gint y, } } +static void rofi_quit_user_callback(RofiViewState *state) { + if (state->retv & MENU_OK) { + if (config.on_entry_accepted == NULL) + return; + int fstate = 0; + unsigned int selected = listview_get_selected(state->list_view); + // TODO: handle custom text + if (selected >= state->filtered_lines) + return; + // Pass selected text to custom command + char *text = mode_get_display_value(state->sw, state->line_map[selected], + &fstate, NULL, TRUE); + char **args = NULL; + int argv = 0; + helper_parse_setup(config.on_entry_accepted, &args, &argv, "{entry}", text, + (char *)0); + if (args != NULL) + helper_execute(NULL, args, "", config.on_entry_accepted, NULL); + g_free(text); + } else if (state->retv & MENU_CANCEL) { + if (config.on_menu_canceled == NULL) + return; + helper_execute_command(NULL, config.on_menu_canceled, FALSE, NULL); + } else if (state->retv & MENU_NEXT || state->retv & MENU_PREVIOUS || + state->retv & MENU_QUICK_SWITCH || state->retv & MENU_COMPLETE) { + if (config.on_mode_changed == NULL) + return; + // TODO: pass mode name to custom command + helper_execute_command(NULL, config.on_mode_changed, FALSE, NULL); + } +} + +void rofi_view_maybe_update(RofiViewState *state) { + if (rofi_view_get_completed(state)) { + // Exec custom user commands + rofi_quit_user_callback(state); + // This menu is done. + rofi_view_finalize(state); + // If there a state. (for example error) reload it. + state = rofi_view_get_active(); + + // cleanup, if no more state to display. + if (state == NULL) { + // Quit main-loop. + rofi_quit_main_loop(); + return; + } + } + + // Update if requested. + if (state->refilter) { + rofi_view_refilter(state); + } + rofi_view_update(state, TRUE); + return; +} WidgetTriggerActionResult textbox_button_trigger_action( widget *wid, MouseBindingMouseDefaultAction action, G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y, G_GNUC_UNUSED void *user_data) { @@ -1558,7 +1664,7 @@ static void rofi_view_add_widget(RofiViewState *state, widget *parent_widget, TB_AUTOWIDTH | TB_AUTOHEIGHT, NORMAL, "*", 0, 0); // Add small separator between case indicator and text box. box_add((box *)parent_widget, WIDGET(state->case_indicator), FALSE); - textbox_text(state->case_indicator, get_matching_state()); + textbox_text(state->case_indicator, get_matching_state(state)); } /** * ENTRY BOX @@ -1694,6 +1800,7 @@ RofiViewState *rofi_view_create(Mode *sw, const char *input, state->menu_flags = menu_flags; state->sw = sw; state->selected_line = UINT32_MAX; + state->previous_line = UINT32_MAX; state->retv = MENU_CANCEL; state->distance = NULL; state->quit = FALSE; @@ -1793,6 +1900,18 @@ RofiViewState *rofi_view_create(Mode *sw, const char *input, return state; } +static void rofi_error_user_callback(const char *msg) { + if (config.on_menu_error == NULL) + return; + + char **args = NULL; + int argv = 0; + helper_parse_setup(config.on_menu_error, &args, &argv, "{error}", msg, + (char *)0); + if (args != NULL) + helper_execute(NULL, args, "", config.on_menu_error, NULL); +} + int rofi_view_error_dialog(const char *msg, int markup) { RofiViewState *state = __rofi_view_state_create(); state->retv = MENU_CANCEL; @@ -1836,6 +1955,9 @@ int rofi_view_error_dialog(const char *msg, int markup) { } #endif + // Exec custom command + rofi_error_user_callback(msg); + // Set it as current window. rofi_view_set_active(state); return TRUE; @@ -1905,10 +2027,38 @@ void rofi_view_workers_finalize(void) { } Mode *rofi_view_get_mode(RofiViewState *state) { return state->sw; } +static gboolean rofi_view_overlay_timeout(G_GNUC_UNUSED gpointer user_data) { + RofiViewState *state = rofi_view_get_active(); + if (state) { + widget_disable(WIDGET(state->overlay)); + } + CacheState.overlay_timeout = 0; + rofi_view_queue_redraw(); + return G_SOURCE_REMOVE; +} + +void rofi_view_set_overlay_timeout(RofiViewState *state, const char *text) { + if (state->overlay == NULL || state->list_view == NULL) { + return; + } + if (text == NULL) { + widget_disable(WIDGET(state->overlay)); + return; + } + rofi_view_set_overlay(state, text); + int timeout = rofi_theme_get_integer(WIDGET(state->overlay), "timeout", 3); + CacheState.overlay_timeout = + g_timeout_add_seconds(timeout, rofi_view_overlay_timeout, state); +} + void rofi_view_set_overlay(RofiViewState *state, const char *text) { if (state->overlay == NULL || state->list_view == NULL) { return; } + if (CacheState.overlay_timeout > 0) { + g_source_remove(CacheState.overlay_timeout); + CacheState.overlay_timeout = 0; + } if (text == NULL) { widget_disable(WIDGET(state->overlay)); return; @@ -1965,10 +2115,6 @@ void rofi_view_update(RofiViewState *state, gboolean qr) { proxy->update(state, qr); } -void rofi_view_maybe_update(RofiViewState *state) { - proxy->maybe_update(state); -} - void rofi_view_temp_configure_notify(RofiViewState *state, xcb_configure_notify_event_t *xce) { proxy->temp_configure_notify(state, xce); diff --git a/source/wayland/view.c b/source/wayland/view.c index 5ec93274b..796b53abc 100644 --- a/source/wayland/view.c +++ b/source/wayland/view.c @@ -64,8 +64,6 @@ */ static void wayland_rofi_view_update(RofiViewState *state, gboolean qr); -static void wayland_rofi_view_maybe_update(RofiViewState *state); - /** * Structure holding some state */ @@ -119,7 +117,7 @@ static gboolean wayland_rofi_view_repaint(G_GNUC_UNUSED void *data) { // Repaint the view (if needed). // After a resize the edit_pixmap surface might not contain anything // anymore. If we already re-painted, this does nothing. - wayland_rofi_view_maybe_update(state); + rofi_view_maybe_update(state); WlState.repaint_source = 0; } return G_SOURCE_REMOVE; @@ -191,7 +189,7 @@ static gboolean wayland_rofi_view_reload_idle(G_GNUC_UNUSED gpointer data) { state->reload = TRUE; state->refilter = TRUE; - wayland_rofi_view_maybe_update(state); + rofi_view_maybe_update(state); } WlState.idle_timeout = 0; return G_SOURCE_REMOVE; @@ -369,29 +367,6 @@ static void wayland_rofi_view_update(RofiViewState *state, gboolean qr) { */ void process_result(RofiViewState *state); -static void wayland_rofi_view_maybe_update(RofiViewState *state) { - if (rofi_view_get_completed(state)) { - // This menu is done. - rofi_view_finalize(state); - // If there a state. (for example error) reload it. - state = rofi_view_get_active(); - - // cleanup, if no more state to display. - if (state == NULL) { - // Quit main-loop. - rofi_quit_main_loop(); - return; - } - } - - // Update if requested. - if (state->refilter) { - rofi_view_refilter(state); - } - wayland_rofi_view_update(state, TRUE); - return; -} - static void wayland_rofi_view_frame_callback(void) { if (WlState.repaint_source == 0) { WlState.repaint_source = g_idle_add_full( @@ -427,14 +402,18 @@ static void wayland_rofi_view_cleanup(void) { g_source_remove(WlState.idle_timeout); WlState.idle_timeout = 0; } - if (CacheState.user_timeout > 0) { - g_source_remove(CacheState.user_timeout); - CacheState.user_timeout = 0; - } if (CacheState.refilter_timeout > 0) { g_source_remove(CacheState.refilter_timeout); CacheState.refilter_timeout = 0; } + if (CacheState.overlay_timeout) { + g_source_remove(CacheState.overlay_timeout); + CacheState.overlay_timeout = 0; + } + if (CacheState.user_timeout > 0) { + g_source_remove(CacheState.user_timeout); + CacheState.user_timeout = 0; + } if (WlState.repaint_source > 0) { g_source_remove(WlState.repaint_source); WlState.repaint_source = 0; @@ -458,7 +437,6 @@ static void wayland_rofi_view_pool_refresh(void) { static view_proxy view_ = { .update = wayland_rofi_view_update, - .maybe_update = wayland_rofi_view_maybe_update, .temp_configure_notify = NULL, .temp_click_to_exit = NULL, .frame_callback = wayland_rofi_view_frame_callback, diff --git a/source/widgets/textbox.c b/source/widgets/textbox.c index 914dd4b06..ff83de0e0 100644 --- a/source/widgets/textbox.c +++ b/source/widgets/textbox.c @@ -250,6 +250,15 @@ textbox *textbox_create(widget *parent, WidgetType type, const char *name, tb->placeholder = g_markup_escape_text(placeholder, -1); } } + + const char *password_mask_char = + rofi_theme_get_string(WIDGET(tb), "password-mask", NULL); + if (password_mask_char == NULL || (*password_mask_char) == '\0') { + tb->password_mask_char = "*"; + } else { + tb->password_mask_char = password_mask_char; + } + textbox_text(tb, txt ? txt : ""); textbox_cursor_end(tb); @@ -336,11 +345,14 @@ static void __textbox_update_pango_text(textbox *tb) { } tb->show_placeholder = FALSE; if ((tb->flags & TB_PASSWORD) == TB_PASSWORD) { - size_t l = g_utf8_strlen(tb->text, -1); - char string[l + 1]; - memset(string, '*', l); - string[l] = '\0'; - pango_layout_set_text(tb->layout, string, l); + size_t text_len = g_utf8_strlen(tb->text, -1); + size_t mask_len = strlen(tb->password_mask_char); + char string[text_len * mask_len + 1]; + for (size_t offset = 0; offset < text_len * mask_len; offset += mask_len) { + memcpy(string + offset, tb->password_mask_char, mask_len); + } + string[text_len * mask_len] = '\0'; + pango_layout_set_text(tb->layout, string, -1); } else if (tb->flags & TB_MARKUP || tb->tbft & MARKUP) { pango_layout_set_markup(tb->layout, tb->text, -1); } else { @@ -488,7 +500,6 @@ static void textbox_draw(widget *wid, cairo_t *draw) { return; } textbox *tb = (textbox *)wid; - int dot_offset = 0; if (tb->changed) { __textbox_update_pango_text(tb); @@ -521,7 +532,7 @@ static void textbox_draw(widget *wid, cairo_t *draw) { { int rem = MAX(0, tb->widget.w - widget_padding_get_padding_width(WIDGET(tb)) - - line_width - dot_offset); + line_width); switch (pango_layout_get_alignment(tb->layout)) { case PANGO_ALIGN_CENTER: x = rem * (tb->xalign - 0.5); @@ -530,7 +541,7 @@ static void textbox_draw(widget *wid, cairo_t *draw) { x = rem * (tb->xalign - 1.0); break; default: - x = rem * tb->xalign + dot_offset; + x = rem * tb->xalign; break; } x += widget_padding_get_left(WIDGET(tb)); @@ -541,11 +552,20 @@ static void textbox_draw(widget *wid, cairo_t *draw) { // We want to place the cursor based on the text shown. const char *text = pango_layout_get_text(tb->layout); // Clamp the position, should not be needed, but we are paranoid. - int cursor_offset = MIN(tb->cursor, g_utf8_strlen(text, -1)); + size_t cursor_offset; + + if ((tb->flags & TB_PASSWORD) == TB_PASSWORD) { + // Calculate cursor position based on mask length + size_t mask_len = strlen(tb->password_mask_char); + cursor_offset = MIN(tb->cursor * mask_len, strlen(text)); + } else { + cursor_offset = MIN(tb->cursor, g_utf8_strlen(text, -1)); + // convert to byte location. + char *offset = g_utf8_offset_to_pointer(text, cursor_offset); + cursor_offset = offset - text; + } PangoRectangle pos; - // convert to byte location. - char *offset = g_utf8_offset_to_pointer(text, cursor_offset); - pango_layout_get_cursor_pos(tb->layout, offset - text, &pos, NULL); + pango_layout_get_cursor_pos(tb->layout, cursor_offset, &pos, NULL); int cursor_x = pos.x / PANGO_SCALE; int cursor_y = pos.y / PANGO_SCALE; int cursor_height = pos.height / PANGO_SCALE; diff --git a/source/xcb/view.c b/source/xcb/view.c index 55ae4c3cb..2c7056e7f 100644 --- a/source/xcb/view.c +++ b/source/xcb/view.c @@ -310,28 +310,6 @@ static void xcb_rofi_view_update(RofiViewState *state, gboolean qr) { } } -static void xcb_rofi_view_maybe_update(RofiViewState *state) { - if (rofi_view_get_completed(state)) { - // This menu is done. - rofi_view_finalize(state); - // If there a state. (for example error) reload it. - state = rofi_view_get_active(); - - // cleanup, if no more state to display. - if (state == NULL) { - // Quit main-loop. - rofi_quit_main_loop(); - return; - } - } - - // Update if requested. - if (state->refilter) { - rofi_view_refilter(state); - } - rofi_view_update(state, TRUE); - return; -} /** * Calculates the window position @@ -962,14 +940,18 @@ static void xcb_rofi_view_cleanup(void) { g_source_remove(XcbState.idle_timeout); XcbState.idle_timeout = 0; } - if (CacheState.user_timeout > 0) { - g_source_remove(CacheState.user_timeout); - CacheState.user_timeout = 0; - } if (CacheState.refilter_timeout > 0) { g_source_remove(CacheState.refilter_timeout); CacheState.refilter_timeout = 0; } + if (CacheState.overlay_timeout) { + g_source_remove(CacheState.overlay_timeout); + CacheState.overlay_timeout = 0; + } + if (CacheState.user_timeout > 0) { + g_source_remove(CacheState.user_timeout); + CacheState.user_timeout = 0; + } if (XcbState.repaint_source > 0) { g_source_remove(XcbState.repaint_source); XcbState.repaint_source = 0; @@ -1020,7 +1002,6 @@ static void xcb_rofi_view_set_window_title(const char *title) { static view_proxy view_ = { .update = xcb_rofi_view_update, - .maybe_update = xcb_rofi_view_maybe_update, .temp_configure_notify = xcb_rofi_view_temp_configure_notify, .temp_click_to_exit = xcb_rofi_view_temp_click_to_exit, .frame_callback = xcb_rofi_view_frame_callback, diff --git a/source/xrmoptions.c b/source/xrmoptions.c index 39ac90c41..c2f014298 100644 --- a/source/xrmoptions.c +++ b/source/xrmoptions.c @@ -132,6 +132,43 @@ static XrmOption xrmOptions[] = { "Custom command to generate preview icons", CONFIG_DEFAULT}, + {xrm_String, + "on-selection-changed", + {.str = &config.on_selection_changed}, + NULL, + "Custom command to call when menu selection changes", + CONFIG_DEFAULT}, + {xrm_String, + "on-mode-changed", + {.str = &config.on_mode_changed}, + NULL, + "Custom command to call when menu mode changes", + CONFIG_DEFAULT}, + {xrm_String, + "on-entry-accepted", + {.str = &config.on_entry_accepted}, + NULL, + "Custom command to call when menu entry is accepted", + CONFIG_DEFAULT}, + {xrm_String, + "on-menu-canceled", + {.str = &config.on_menu_canceled}, + NULL, + "Custom command to call when menu is canceled", + CONFIG_DEFAULT}, + {xrm_String, + "on-menu-error", + {.str = &config.on_menu_error}, + NULL, + "Custom command to call when menu finds errors", + CONFIG_DEFAULT}, + {xrm_String, + "on-screenshot-taken", + {.str = &config.on_screenshot_taken}, + NULL, + "Custom command to call when menu screenshot is taken", + CONFIG_DEFAULT}, + {xrm_String, "terminal", {.str = &config.terminal_emulator}, @@ -249,6 +286,12 @@ static XrmOption xrmOptions[] = { NULL, "Set case-sensitivity", CONFIG_DEFAULT}, + {xrm_Boolean, + "case-smart", + {.num = &config.case_smart}, + NULL, + "Set smartcase like vim (determine case-sensitivity by input)", + CONFIG_DEFAULT}, {xrm_Boolean, "cycle", {.num = &config.cycle}, diff --git a/subprojects/libnkutils b/subprojects/libnkutils index 72bd7fb07..2f220a40a 160000 --- a/subprojects/libnkutils +++ b/subprojects/libnkutils @@ -1 +1 @@ -Subproject commit 72bd7fb07f627a864e724639eea9fab6cccbd77c +Subproject commit 2f220a40ad32cf51b6b7d7ae83ab641a3ae76693 diff --git a/test/helper-test.c b/test/helper-test.c index ce63e3031..6f33f2d39 100644 --- a/test/helper-test.c +++ b/test/helper-test.c @@ -139,25 +139,25 @@ int main(int argc, char **argv) { */ TASSERT(levenshtein("aap", g_utf8_strlen("aap", -1), "aap", - g_utf8_strlen("aap", -1)) == 0); + g_utf8_strlen("aap", -1), 0) == 0); TASSERT(levenshtein("aap", g_utf8_strlen("aap", -1), "aap ", - g_utf8_strlen("aap ", -1)) == 1); + g_utf8_strlen("aap ", -1), 0) == 1); TASSERT(levenshtein("aap ", g_utf8_strlen("aap ", -1), "aap", - g_utf8_strlen("aap", -1)) == 1); + g_utf8_strlen("aap", -1), 0) == 1); TASSERTE(levenshtein("aap", g_utf8_strlen("aap", -1), "aap noot", - g_utf8_strlen("aap noot", -1)), + g_utf8_strlen("aap noot", -1), 0), 5u); TASSERTE(levenshtein("aap", g_utf8_strlen("aap", -1), "noot aap", - g_utf8_strlen("noot aap", -1)), + g_utf8_strlen("noot aap", -1), 0), 5u); TASSERTE(levenshtein("aap", g_utf8_strlen("aap", -1), "noot aap mies", - g_utf8_strlen("noot aap mies", -1)), + g_utf8_strlen("noot aap mies", -1), 0), 10u); TASSERTE(levenshtein("noot aap mies", g_utf8_strlen("noot aap mies", -1), - "aap", g_utf8_strlen("aap", -1)), + "aap", g_utf8_strlen("aap", -1), 0), 10u); TASSERTE(levenshtein("otp", g_utf8_strlen("otp", -1), "noot aap", - g_utf8_strlen("noot aap", -1)), + g_utf8_strlen("noot aap", -1), 0), 5u); /** * Quick converision check. @@ -189,20 +189,48 @@ int main(int argc, char **argv) { } { TASSERTL( - rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "aap noot mies", 12), + rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "aap noot mies", 12, 0), -605); - TASSERTL(rofi_scorer_fuzzy_evaluate("anm", 3, "aap noot mies", 12), -155); - TASSERTL(rofi_scorer_fuzzy_evaluate("blu", 3, "aap noot mies", 12), + TASSERTL(rofi_scorer_fuzzy_evaluate("anm", 3, "aap noot mies", 12, 0), + -155); + TASSERTL(rofi_scorer_fuzzy_evaluate("blu", 3, "aap noot mies", 12, 0), 1073741824); - config.case_sensitive = TRUE; - TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12), + TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12, 1), 1073741754); - config.case_sensitive = FALSE; - TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12), -155); - TASSERTL(rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "Anm", 3), + TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12, 0), + -155); + TASSERTL(rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "Anm", 3, 0), 1073741824); } + /** + * Case sensitivity + */ + { + int case_smart = config.case_smart; + int case_sensitive = config.case_sensitive; + { + config.case_smart = FALSE; + config.case_sensitive = FALSE; + TASSERT(parse_case_sensitivity("all lower case 你好") == 0); + TASSERT(parse_case_sensitivity("not All lowEr Case 你好") == 0); + config.case_sensitive = TRUE; + TASSERT(parse_case_sensitivity("all lower case 你好") == 1); + TASSERT(parse_case_sensitivity("not All lowEr Case 你好") == 1); + } + { + config.case_smart = TRUE; + config.case_sensitive = TRUE; + TASSERT(parse_case_sensitivity("all lower case") == 0); + TASSERT(parse_case_sensitivity("AAAAAAAAAAAA") == 1); + config.case_sensitive = FALSE; + TASSERT(parse_case_sensitivity("all lower case 你好") == 0); + TASSERT(parse_case_sensitivity("not All lowEr Case 你好") == 1); + } + config.case_smart = case_smart; + config.case_sensitive = case_sensitive; + } + char *a; a = helper_string_replace_if_exists( "{terminal} [-t {title} blub ]-e {cmd}", "{cmd}", "aap", "{title}", diff --git a/test/mode-test.c b/test/mode-test.c index 4ba7548d7..e4b31b080 100644 --- a/test/mode-test.c +++ b/test/mode-test.c @@ -125,7 +125,7 @@ END_TEST START_TEST(test_mode_num_items) { unsigned int rows = mode_get_num_entries(&help_keys_mode); - ck_assert_int_eq(rows, 79); + ck_assert_int_eq(rows, 81); for (unsigned int i = 0; i < rows; i++) { int state = 0; GList *list = NULL;