From 132eb020ed6e0af4a79fb907fb09afb193d17518 Mon Sep 17 00:00:00 2001 From: Fixerer Date: Wed, 17 Sep 2025 01:04:15 +0200 Subject: [PATCH 1/7] Make clicking on a match select it on close-on-click (Fixes #149) Modify `PluginMatch` box to handle GestureClick and claim the event, manually selecting the row. Then also output a `Clicked` event, which is passed on to the main app events. On this event, if `close-on-click` is true, do perform the `Select` action, picking the item and running its result. This changes the behaviour (for close-on-click=true) on clicking menu items, selecting them instead of just closing the application. --- anyrun/src/main.rs | 5 +++++ anyrun/src/plugin_box.rs | 32 ++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/anyrun/src/main.rs b/anyrun/src/main.rs index 20a4b81..a443dd3 100644 --- a/anyrun/src/main.rs +++ b/anyrun/src/main.rs @@ -472,6 +472,11 @@ impl Component for App { } } } + AppMsg::PluginOutput(PluginBoxOutput::MatchClicked) => { + if self.config.close_on_click { + sender.input(AppMsg::Action(Action::Select)); + } + } } self.update_view(widgets, sender); } diff --git a/anyrun/src/plugin_box.rs b/anyrun/src/plugin_box.rs index 5a14dd1..266fdca 100644 --- a/anyrun/src/plugin_box.rs +++ b/anyrun/src/plugin_box.rs @@ -3,7 +3,7 @@ use std::{path::PathBuf, rc::Rc}; use abi_stable::std_types::{ROption, RVec}; use anyrun_interface::{Match, PluginRef}; use gtk::{glib, pango, prelude::*}; -use gtk4 as gtk; +use gtk4::{self as gtk, ListBox}; use relm4::prelude::*; use crate::Config; @@ -14,11 +14,16 @@ pub struct PluginMatch { config: Rc, } +#[derive(Debug)] +pub enum MatchOutput { + Clicked, +} + #[relm4::factory(pub)] impl FactoryComponent for PluginMatch { type Init = (Match, Rc); type Input = (); - type Output = (); + type Output = MatchOutput; type CommandOutput = (); type ParentWidget = gtk::ListBox; view! { @@ -26,6 +31,18 @@ impl FactoryComponent for PluginMatch { set_css_classes: &["match"], set_height_request: 32, gtk::Box { + add_controller = gtk::GestureClick { + connect_pressed[sender, root] => move |gesture, _, _, _| { + if let Some(binding) = root.parent() { + if let Some(list_box) = binding.downcast_ref::() { + list_box.select_row(Some(&root)); + } + } + gesture.set_state(gtk::EventSequenceState::Claimed); + sender.output(MatchOutput::Clicked).unwrap(); + } + }, + set_orientation: gtk::Orientation::Horizontal, set_spacing: 10, set_css_classes: &["match"], @@ -77,7 +94,7 @@ impl FactoryComponent for PluginMatch { _index: &Self::Index, root: Self::Root, _returned_widget: &::ReturnedWidget, - _sender: FactorySender, + sender: FactorySender, ) -> Self::Widgets { let widgets = view_output!(); @@ -140,6 +157,7 @@ pub enum PluginBoxInput { pub enum PluginBoxOutput { MatchesLoaded, RowSelected(::Index), + MatchClicked, } #[relm4::factory(pub)] @@ -213,11 +231,13 @@ impl FactoryComponent for PluginBox { fn init_model( (plugin, config): Self::Init, _index: &Self::Index, - _sender: FactorySender, + sender: FactorySender, ) -> Self { - let matches = FactoryVecDeque::builder() + let matches = FactoryVecDeque::::builder() .launch(gtk::ListBox::default()) - .detach(); + .forward(sender.output_sender(), |output| match output { + MatchOutput::Clicked => PluginBoxOutput::MatchClicked, + }); Self { plugin, From 632924c61db85615359408c46c5e5b3f2c8264f1 Mon Sep 17 00:00:00 2001 From: Fixerer Date: Wed, 17 Sep 2025 17:05:47 +0200 Subject: [PATCH 2/7] Change import style --- anyrun/src/plugin_box.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anyrun/src/plugin_box.rs b/anyrun/src/plugin_box.rs index 266fdca..f3389e7 100644 --- a/anyrun/src/plugin_box.rs +++ b/anyrun/src/plugin_box.rs @@ -3,7 +3,7 @@ use std::{path::PathBuf, rc::Rc}; use abi_stable::std_types::{ROption, RVec}; use anyrun_interface::{Match, PluginRef}; use gtk::{glib, pango, prelude::*}; -use gtk4::{self as gtk, ListBox}; +use gtk4 as gtk; use relm4::prelude::*; use crate::Config; @@ -34,7 +34,7 @@ impl FactoryComponent for PluginMatch { add_controller = gtk::GestureClick { connect_pressed[sender, root] => move |gesture, _, _, _| { if let Some(binding) = root.parent() { - if let Some(list_box) = binding.downcast_ref::() { + if let Some(list_box) = binding.downcast_ref::() { list_box.select_row(Some(&root)); } } From 872f8d01306971d00556bf005b71e5cd03dee8d4 Mon Sep 17 00:00:00 2001 From: Fixerer Date: Wed, 17 Sep 2025 17:43:08 +0200 Subject: [PATCH 3/7] Make selecting item on click default behaviour even when not close-on-click --- anyrun/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/anyrun/src/main.rs b/anyrun/src/main.rs index a443dd3..fc09b0d 100644 --- a/anyrun/src/main.rs +++ b/anyrun/src/main.rs @@ -473,9 +473,7 @@ impl Component for App { } } AppMsg::PluginOutput(PluginBoxOutput::MatchClicked) => { - if self.config.close_on_click { - sender.input(AppMsg::Action(Action::Select)); - } + sender.input(AppMsg::Action(Action::Select)); } } self.update_view(widgets, sender); From 973b555145cf58521e38632135cc7504fcd5b337 Mon Sep 17 00:00:00 2001 From: Fixerer Date: Wed, 17 Sep 2025 21:04:21 +0200 Subject: [PATCH 4/7] Add mouse action and binds instead --- anyrun/src/config.rs | 33 ++++++++++++++++++++++++++++++++ anyrun/src/main.rs | 40 ++++++++++++++++++++++++++++++++++++--- anyrun/src/plugin_box.rs | 38 +++++++++++++++++++++++-------------- examples/config.ron | 41 +++++++++++++++++++++++++--------------- 4 files changed, 120 insertions(+), 32 deletions(-) diff --git a/anyrun/src/config.rs b/anyrun/src/config.rs index 8690f5c..da76ff1 100644 --- a/anyrun/src/config.rs +++ b/anyrun/src/config.rs @@ -40,6 +40,10 @@ pub struct Config { #[config_args(skip)] #[serde(default = "Config::default_keybinds")] pub keybinds: Vec, + + #[config_args(skip)] + #[serde(default = "Config::default_mousebinds")] + pub mousebinds: Vec, } impl Config { fn default_x() -> RelativeNum { @@ -103,6 +107,19 @@ impl Config { }, ] } + + fn default_mousebinds() -> Vec { + vec![ + Mousebind { + button: MouseButton::Primary, + action: Action::Select, + }, + Mousebind { + button: MouseButton::Secondary, + action: Action::Nop, + }, + ] + } } impl Default for Config { fn default() -> Self { @@ -121,6 +138,7 @@ impl Default for Config { layer: Self::default_layer(), keyboard_mode: Self::default_keyboard_mode(), keybinds: Self::default_keybinds(), + mousebinds: Self::default_mousebinds(), } } } @@ -173,6 +191,21 @@ pub enum Action { Select, Up, Down, + Nop, +} + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +pub enum MouseButton { + Primary, + Secondary, + Middle, + Unknown, +} + +#[derive(Deserialize, Debug, Clone, Copy)] +pub struct Mousebind { + pub button: MouseButton, + pub action: Action, } #[derive(Deserialize, Clone)] diff --git a/anyrun/src/main.rs b/anyrun/src/main.rs index fc09b0d..6720038 100644 --- a/anyrun/src/main.rs +++ b/anyrun/src/main.rs @@ -16,7 +16,7 @@ use relm4::prelude::*; use wl_clipboard_rs::copy; use crate::{ - config::{Action, Config, ConfigArgs, Keybind}, + config::{Action, Config, ConfigArgs, Keybind, Mousebind}, plugin_box::{PluginBox, PluginBoxInput, PluginBoxOutput, PluginMatch}, }; @@ -106,6 +106,22 @@ impl App { (i, plugin, plugin_match) }) } + + fn select_row(&self, plugin_ind: DynamicIndex, match_ind: DynamicIndex) { + // Select match row + for (i, plugin) in self.plugins.iter().enumerate() { + if i == plugin_ind.current_index() { + for (j, plugin_match) in plugin.matches.iter().enumerate() { + if j == match_ind.current_index() { + plugin + .matches + .widget() + .select_row(Option::<>k::ListBoxRow>::Some(&plugin_match.row)); + } + } + } + } + } } #[relm4::component] @@ -377,6 +393,7 @@ impl Component for App { } } AppMsg::Action(action) => match action { + Action::Nop => {} Action::Close => { root.close(); relm4::main_application().quit(); @@ -472,8 +489,25 @@ impl Component for App { } } } - AppMsg::PluginOutput(PluginBoxOutput::MatchClicked) => { - sender.input(AppMsg::Action(Action::Select)); + AppMsg::PluginOutput(PluginBoxOutput::MouseAction(button, match_ind, plugin_ind)) => { + // Handle binding + if let Some(Mousebind { action, .. }) = self + .config + .mousebinds + .iter() + .find(|mousebind| mousebind.button == button) + { + // Potentially select row + match action { + Action::Select | Action::Nop => self.select_row(plugin_ind, match_ind), + _ => { + // Don't select row for other actions + } + }; + + // Perform action + sender.input(AppMsg::Action(*action)); + } } } self.update_view(widgets, sender); diff --git a/anyrun/src/plugin_box.rs b/anyrun/src/plugin_box.rs index f3389e7..6823eed 100644 --- a/anyrun/src/plugin_box.rs +++ b/anyrun/src/plugin_box.rs @@ -6,7 +6,7 @@ use gtk::{glib, pango, prelude::*}; use gtk4 as gtk; use relm4::prelude::*; -use crate::Config; +use crate::{config::MouseButton, Config}; pub struct PluginMatch { pub content: Match, @@ -16,7 +16,7 @@ pub struct PluginMatch { #[derive(Debug)] pub enum MatchOutput { - Clicked, + MouseAction(MouseButton, ::Index), } #[relm4::factory(pub)] @@ -31,15 +31,18 @@ impl FactoryComponent for PluginMatch { set_css_classes: &["match"], set_height_request: 32, gtk::Box { + add_controller = gtk::GestureClick { - connect_pressed[sender, root] => move |gesture, _, _, _| { - if let Some(binding) = root.parent() { - if let Some(list_box) = binding.downcast_ref::() { - list_box.select_row(Some(&root)); - } - } + set_button: 0, + connect_pressed[sender, index] => move |gesture, _, _, _| { gesture.set_state(gtk::EventSequenceState::Claimed); - sender.output(MatchOutput::Clicked).unwrap(); + let button: MouseButton = match gesture.current_button() { + gtk::gdk::BUTTON_PRIMARY => MouseButton::Primary, + gtk::gdk::BUTTON_SECONDARY => MouseButton::Secondary, + gtk::gdk::BUTTON_MIDDLE => MouseButton::Middle, + _ => MouseButton::Unknown, + }; + sender.output(MatchOutput::MouseAction(button, index.clone())).unwrap(); } }, @@ -91,7 +94,7 @@ impl FactoryComponent for PluginMatch { fn init_widgets( &mut self, - _index: &Self::Index, + index: &Self::Index, root: Self::Root, _returned_widget: &::ReturnedWidget, sender: FactorySender, @@ -157,7 +160,11 @@ pub enum PluginBoxInput { pub enum PluginBoxOutput { MatchesLoaded, RowSelected(::Index), - MatchClicked, + MouseAction( + MouseButton, + ::Index, + ::Index, + ), } #[relm4::factory(pub)] @@ -230,13 +237,16 @@ impl FactoryComponent for PluginBox { fn init_model( (plugin, config): Self::Init, - _index: &Self::Index, + index: &Self::Index, sender: FactorySender, ) -> Self { + let ind = index.clone(); let matches = FactoryVecDeque::::builder() .launch(gtk::ListBox::default()) - .forward(sender.output_sender(), |output| match output { - MatchOutput::Clicked => PluginBoxOutput::MatchClicked, + .forward(sender.output_sender(), move |output| match output { + MatchOutput::MouseAction(button, row) => { + PluginBoxOutput::MouseAction(button, row, ind.clone()) + } }); Self { diff --git a/examples/config.ron b/examples/config.ron index 78196f5..48a6c4b 100644 --- a/examples/config.ron +++ b/examples/config.ron @@ -2,7 +2,7 @@ Config( // Position/size fields use an enum for the value, it can be either: // Absolute(n): The absolute value in pixels // Fraction(n): A fraction of the width or height of the full screen (depends on exclusive zones and the settings related to them) window respectively - + // The horizontal position, adjusted so that Relative(0.5) always centers the runner x: Fraction(0.5), @@ -15,18 +15,18 @@ Config( // The minimum height of the runner, the runner will expand to fit all the entries // NOTE: If this is set to 0, the window will never shrink after being expanded height: Absolute(1), - - // Hide match and plugin info icons - hide_icons: false, - // ignore exclusive zones, f.e. Waybar - ignore_exclusive_zones: false, + // Hide match and plugin info icons + hide_icons: false, + + // ignore exclusive zones, f.e. Waybar + ignore_exclusive_zones: false, + + // Layer shell layer: Background, Bottom, Top, Overlay + layer: Overlay, - // Layer shell layer: Background, Bottom, Top, Overlay - layer: Overlay, - // Hide the plugin info panel - hide_plugin_info: false, + hide_plugin_info: false, // Close window when a click outside the main box is received close_on_click: false, @@ -36,7 +36,7 @@ Config( // Limit amount of entries shown in total max_entries: None, - + // List of plugins to be loaded by default, can be specified with a relative path to be loaded from the // `/plugins` directory or with an absolute path to just load the file the path points to. // @@ -52,19 +52,30 @@ Config( keybinds: [ Keybind( key: "Return", - action: Select, + action: Select, ), Keybind( key: "Up", - action: Up, + action: Up, ), Keybind( key: "Down", - action: Down, + action: Down, ), Keybind( key: "Escape", - action: Close, + action: Close, + ), + ], + + mousebinds: [ + Mousebind( + button: Primary, + action: Select, + ), + Mousebind( + button: Secondary, + action: Nop, ), ], ) From 26dfae2ee59b5493c67d245f17224f44294a4a8f Mon Sep 17 00:00:00 2001 From: Fixerer Date: Thu, 18 Sep 2025 14:31:46 +0200 Subject: [PATCH 5/7] Allow binding of other mouse keys --- anyrun/src/config.rs | 2 +- anyrun/src/plugin_box.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/anyrun/src/config.rs b/anyrun/src/config.rs index da76ff1..ebf88b9 100644 --- a/anyrun/src/config.rs +++ b/anyrun/src/config.rs @@ -199,7 +199,7 @@ pub enum MouseButton { Primary, Secondary, Middle, - Unknown, + Unknown(u32), } #[derive(Deserialize, Debug, Clone, Copy)] diff --git a/anyrun/src/plugin_box.rs b/anyrun/src/plugin_box.rs index 6823eed..7fdaab0 100644 --- a/anyrun/src/plugin_box.rs +++ b/anyrun/src/plugin_box.rs @@ -40,7 +40,7 @@ impl FactoryComponent for PluginMatch { gtk::gdk::BUTTON_PRIMARY => MouseButton::Primary, gtk::gdk::BUTTON_SECONDARY => MouseButton::Secondary, gtk::gdk::BUTTON_MIDDLE => MouseButton::Middle, - _ => MouseButton::Unknown, + other => MouseButton::Unknown(other), }; sender.output(MatchOutput::MouseAction(button, index.clone())).unwrap(); } From d6db78a4f29b625b962a16a63785b11591bdda97 Mon Sep 17 00:00:00 2001 From: Fixerer Date: Thu, 18 Sep 2025 14:43:40 +0200 Subject: [PATCH 6/7] Add home-manager option for mousebinds --- nix/modules/home-manager.nix | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/nix/modules/home-manager.nix b/nix/modules/home-manager.nix index d5d4c9d..2c341ce 100644 --- a/nix/modules/home-manager.nix +++ b/nix/modules/home-manager.nix @@ -224,6 +224,7 @@ in "select" "up" "down" + "nop" ]; }; }; @@ -232,6 +233,34 @@ in default = null; }; + mousebinds = mkOption { + type = nullOr ( + listOf (submodule { + options = { + button = mkOption { + type = str; + description = '' + Mouse button name + + Primary/Secondary/Middle or Unknown() with key code for the mouse button. + ''; + }; + action = mkOption { + type = enum [ + "close" + "select" + "up" + "down" + "nop" + ]; + }; + }; + }) + ); + default = null; + }; + + extraLines = mkOption { type = nullOr lines; default = null; @@ -328,6 +357,22 @@ in '') cfg.config.keybinds }], ''; + + mousebinds = + if cfg.config.mousebinds == null then + "" + else + '' + mousebinds: [ + ${ + concatMapStringsSep "\n" (x: '' + Mousebind( + button: ${capitalize x.button}, + action: ${capitalize x.action}, + ), + '') cfg.config.mousebinds + }], + ''; in { assertions = [ @@ -382,6 +427,7 @@ in plugins: ${toJSON parsedPlugins}, ${optionalString (cfg.config.extraLines != null) cfg.config.extraLines} ${keybinds} + ${mousebinds} ) ''; } From d6de6613b026f4247d11faaf5b08d04ec845dbe1 Mon Sep 17 00:00:00 2001 From: Fixerer Date: Thu, 18 Sep 2025 15:06:58 +0200 Subject: [PATCH 7/7] Deselect other plugins --- anyrun/src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/anyrun/src/main.rs b/anyrun/src/main.rs index 6720038..5b11f2c 100644 --- a/anyrun/src/main.rs +++ b/anyrun/src/main.rs @@ -119,6 +119,11 @@ impl App { .select_row(Option::<>k::ListBoxRow>::Some(&plugin_match.row)); } } + } else { + plugin + .matches + .widget() + .select_row(Option::<>k::ListBoxRow>::None); } } }