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 30770e7fa..c899d0f92 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", 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/include/settings.h b/include/settings.h index 5da4279fc..d5be2dfe9 100644 --- a/include/settings.h +++ b/include/settings.h @@ -64,6 +64,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 */ diff --git a/include/view-internal.h b/include/view-internal.h index b6738a571..da6bd09a1 100644 --- a/include/view-internal.h +++ b/include/view-internal.h @@ -90,6 +90,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 */ diff --git a/source/view.c b/source/view.c index d0ee4d995..e1054bee5 100644 --- a/source/view.c +++ b/source/view.c @@ -212,6 +212,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. */ @@ -271,6 +283,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); } @@ -1285,9 +1298,30 @@ 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; @@ -1295,7 +1329,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, ""); } @@ -1882,7 +1915,6 @@ static void rofi_view_trigger_global_action(KeyBindingAction action) { // Nothing entered and nothing selected. state->retv = MENU_CUSTOM_INPUT; } - state->quit = TRUE; break; } @@ -2086,8 +2118,43 @@ 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. @@ -2482,6 +2549,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; @@ -2571,6 +2639,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; @@ -2608,6 +2688,9 @@ int rofi_view_error_dialog(const char *msg, int markup) { sn_launchee_context_complete(xcb->sncontext); } + // Exec custom command + rofi_error_user_callback(msg); + // Set it as current window. rofi_view_set_active(state); return TRUE; diff --git a/source/xrmoptions.c b/source/xrmoptions.c index 08cbf9c9a..077bf0ec3 100644 --- a/source/xrmoptions.c +++ b/source/xrmoptions.c @@ -136,6 +136,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},