aboutsummaryrefslogtreecommitdiff
path: root/src/config
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 03:56:48 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 04:16:36 +0300
commit2fcf9b678d0f154b2d41aa5fafa25aaa73ec7f17 (patch)
treecbb53f64ebfbf1fef079dbb996deb994bde06ee8 /src/config
parent3ce4212e5e77d816ae02d71912f0b1b4925d7736 (diff)
feat: add configurable right-click passthrough
refs #148
Diffstat (limited to 'src/config')
-rw-r--r--src/config/model.rs135
1 files changed, 134 insertions, 1 deletions
diff --git a/src/config/model.rs b/src/config/model.rs
index 77af2f75..0776e09b 100644
--- a/src/config/model.rs
+++ b/src/config/model.rs
@@ -1,6 +1,7 @@
use std::num::NonZeroUsize;
-use serde::{Deserialize, Deserializer, Serialize};
+use crossterm::event::KeyModifiers;
+use serde::{de, Deserialize, Deserializer, Serialize};
use super::{
BindingConfig, CommandKeybindConfig, SoundConfig, ThemeConfig, DEFAULT_MOBILE_WIDTH_THRESHOLD,
@@ -34,6 +35,59 @@ impl AgentPanelScopeConfig {
}
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct RightClickPassthroughModifierConfig(Option<KeyModifiers>);
+
+impl RightClickPassthroughModifierConfig {
+ pub fn modifiers(self) -> Option<KeyModifiers> {
+ self.0
+ }
+}
+
+impl<'de> Deserialize<'de> for RightClickPassthroughModifierConfig {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ parse_right_click_passthrough_modifier(&value)
+ .map(Self)
+ .ok_or_else(|| {
+ de::Error::custom(
+ "right_click_passthrough_modifier must be empty, off, none, disabled, ctrl/control, alt/option, cmd/command/super, meta, hyper, or a + separated combination without shift",
+ )
+ })
+ }
+}
+
+fn parse_right_click_passthrough_modifier(value: &str) -> Option<Option<KeyModifiers>> {
+ let trimmed = value.trim();
+ if trimmed.is_empty()
+ || trimmed.eq_ignore_ascii_case("off")
+ || trimmed.eq_ignore_ascii_case("none")
+ || trimmed.eq_ignore_ascii_case("disabled")
+ {
+ return Some(None);
+ }
+
+ let mut modifiers = KeyModifiers::empty();
+ for token in trimmed.split('+') {
+ let token = token.trim().to_ascii_lowercase();
+ let modifier = match token.as_str() {
+ "ctrl" | "control" => KeyModifiers::CONTROL,
+ "alt" | "option" => KeyModifiers::ALT,
+ "cmd" | "command" | "super" => KeyModifiers::SUPER,
+ "meta" => KeyModifiers::META,
+ "hyper" => KeyModifiers::HYPER,
+ "shift" => return None,
+ _ => return None,
+ };
+ modifiers |= modifier;
+ }
+
+ (!modifiers.is_empty()).then_some(Some(modifiers))
+}
+
#[derive(Debug, Clone)]
pub struct ToastConfig {
pub delivery: ToastDelivery,
@@ -276,6 +330,8 @@ pub struct UiConfig {
pub mobile_width_threshold: u16,
/// Capture mouse input for Herdr's mouse UI. Default: true.
pub mouse_capture: bool,
+ /// Modifier that lets right-click gestures pass through to pane apps. Empty disables it.
+ pub right_click_passthrough_modifier: RightClickPassthroughModifierConfig,
/// Force a full host-terminal redraw when the outer terminal regains focus. Default: true.
pub redraw_on_focus_gained: bool,
/// Lines to scroll per mouse wheel notch. Default: 3.
@@ -438,6 +494,7 @@ impl Default for UiConfig {
sidebar_max_width: 36,
mobile_width_threshold: DEFAULT_MOBILE_WIDTH_THRESHOLD,
mouse_capture: true,
+ right_click_passthrough_modifier: RightClickPassthroughModifierConfig::default(),
redraw_on_focus_gained: true,
mouse_scroll_lines: None,
confirm_close: true,
@@ -457,6 +514,10 @@ impl UiConfig {
.map(NonZeroUsize::get)
.unwrap_or(DEFAULT_MOUSE_SCROLL_LINES)
}
+
+ pub fn right_click_passthrough_modifiers(&self) -> Option<KeyModifiers> {
+ self.right_click_passthrough_modifier.modifiers()
+ }
}
impl Default for ToastConfig {
@@ -702,6 +763,78 @@ mouse_capture = false
}
#[test]
+ fn right_click_passthrough_modifier_defaults_off_and_parses() {
+ let default_config = Config::default();
+ assert_eq!(default_config.ui.right_click_passthrough_modifiers(), None);
+
+ for value in ["", "off", "none", "disabled"] {
+ let toml = format!(
+ r#"
+[ui]
+right_click_passthrough_modifier = "{value}"
+"#
+ );
+ let config: Config = toml::from_str(&toml).unwrap();
+ assert_eq!(
+ config.ui.right_click_passthrough_modifiers(),
+ None,
+ "value {value:?} should disable passthrough"
+ );
+ }
+
+ for (value, expected) in [
+ ("ctrl", KeyModifiers::CONTROL),
+ ("control", KeyModifiers::CONTROL),
+ ("alt", KeyModifiers::ALT),
+ ("option", KeyModifiers::ALT),
+ ("cmd", KeyModifiers::SUPER),
+ ("command", KeyModifiers::SUPER),
+ ("super", KeyModifiers::SUPER),
+ ("meta", KeyModifiers::META),
+ ("hyper", KeyModifiers::HYPER),
+ ] {
+ let toml = format!(
+ r#"
+[ui]
+right_click_passthrough_modifier = "{value}"
+"#
+ );
+ let config: Config = toml::from_str(&toml).unwrap();
+ assert_eq!(
+ config.ui.right_click_passthrough_modifiers(),
+ Some(expected),
+ "value {value:?} should parse"
+ );
+ }
+
+ let toml = r#"
+[ui]
+right_click_passthrough_modifier = "cmd+alt"
+"#;
+ let config: Config = toml::from_str(toml).unwrap();
+ assert_eq!(
+ config.ui.right_click_passthrough_modifiers(),
+ Some(KeyModifiers::SUPER | KeyModifiers::ALT)
+ );
+ }
+
+ #[test]
+ fn right_click_passthrough_modifier_rejects_shift() {
+ for value in ["shift", "shift+ctrl", "ctrl+", "ctrl++alt", "banana"] {
+ let toml = format!(
+ r#"
+[ui]
+right_click_passthrough_modifier = "{value}"
+"#
+ );
+ assert!(
+ toml::from_str::<Config>(&toml).is_err(),
+ "value {value:?} should be rejected"
+ );
+ }
+ }
+
+ #[test]
fn redraw_on_focus_gained_default_on_and_parse() {
let default_config = Config::default();
assert!(default_config.ui.redraw_on_focus_gained);