From 1956186b50b530f8714d60eedb4231601306a258 Mon Sep 17 00:00:00 2001 From: Matteo Date: Sun, 1 Dec 2024 22:43:07 +0100 Subject: [PATCH 1/4] Implemented custom user command execution on the following menu events: entry selected, entry accepted, menu canceled, menu error, mode changed, screenshot taken --- config/config.c | 12 +++++++ include/settings.h | 12 +++++++ source/view.c | 87 +++++++++++++++++++++++++++++++++++++++++++-- source/xrmoptions.c | 38 +++++++++++++++++++- 4 files changed, 146 insertions(+), 3 deletions(-) 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/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/source/view.c b/source/view.c index e802a7489..912c69218 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,6 +1298,27 @@ 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; + + static int last_index = -1; + if (index >= state->filtered_lines) + return; + if (last_index != index) { + last_index = index; + 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; @@ -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, ""); } @@ -1312,6 +1345,7 @@ static void selection_changed_callback(G_GNUC_UNUSED listview *lv, icon_set_surface(state->icon_current_entry, NULL); } } + selection_changed_user_callback(index, state); } static void update_callback(textbox *t, icon *ico, unsigned int index, void *udata, TextBoxFontType *type, gboolean full) { @@ -1882,7 +1916,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 +2119,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. @@ -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 72e16bc1b..c9f54bc55 100644 --- a/source/xrmoptions.c +++ b/source/xrmoptions.c @@ -128,7 +128,6 @@ static XrmOption xrmOptions[] = { NULL, "Whether to load and show icons", CONFIG_DEFAULT}, - {xrm_String, "preview-cmd", {.str = &config.preview_cmd}, @@ -136,6 +135,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}, From 7ac839b04b5db8cf8ad9b2cc0067b9a38fee5582 Mon Sep 17 00:00:00 2001 From: Matteo Date: Sat, 7 Dec 2024 21:28:18 +0100 Subject: [PATCH 2/4] fixed different signedness comparison warning and compare unfiltered entry index in selection_changed_user_callback --- source/view.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/source/view.c b/source/view.c index 912c69218..b7b856920 100644 --- a/source/view.c +++ b/source/view.c @@ -1302,13 +1302,15 @@ static void selection_changed_user_callback(unsigned int index, RofiViewState *s if (config.on_selection_changed == NULL) return; - static int last_index = -1; + static unsigned int last_index = UINT32_MAX; if (index >= state->filtered_lines) return; - if (last_index != index) { - last_index = index; + + unsigned int real_index = state->line_map[index]; + if (last_index != real_index) { + last_index = real_index; int fstate = 0; - char *text = mode_get_display_value(state->sw, state->line_map[index], + char *text = mode_get_display_value(state->sw, real_index, &fstate, NULL, TRUE); char **args = NULL; int argv = 0; From 2c8c4f1e8afeb6fcc9f175c226534284b190da1d Mon Sep 17 00:00:00 2001 From: Matteo Date: Sun, 8 Dec 2024 00:17:03 +0100 Subject: [PATCH 3/4] track previously selected line in RofiViewState --- include/view-internal.h | 2 ++ source/view.c | 36 +++++++++++++++++------------------- 2 files changed, 19 insertions(+), 19 deletions(-) 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 b7b856920..492c5104c 100644 --- a/source/view.c +++ b/source/view.c @@ -1302,28 +1302,26 @@ static void selection_changed_user_callback(unsigned int index, RofiViewState *s if (config.on_selection_changed == NULL) return; - static unsigned int last_index = UINT32_MAX; - if (index >= state->filtered_lines) - return; - - unsigned int real_index = state->line_map[index]; - if (last_index != real_index) { - last_index = real_index; - int fstate = 0; - char *text = mode_get_display_value(state->sw, real_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); - } + 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; @@ -1347,7 +1345,6 @@ static void selection_changed_callback(G_GNUC_UNUSED listview *lv, icon_set_surface(state->icon_current_entry, NULL); } } - selection_changed_user_callback(index, state); } static void update_callback(textbox *t, icon *ico, unsigned int index, void *udata, TextBoxFontType *type, gboolean full) { @@ -2552,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; From c894f34d3041821cf4f99b294103c4fce3a0fd72 Mon Sep 17 00:00:00 2001 From: giomatfois62 Date: Tue, 31 Dec 2024 16:10:33 +0100 Subject: [PATCH 4/4] added documentation about custom scripts to run on certain actions --- Makefile.am | 1 + doc/meson.build | 1 + doc/rofi-actions.5.markdown | 89 +++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 doc/rofi-actions.5.markdown 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/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 +```