diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-22 23:36:43 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-22 23:36:43 +0300 |
| commit | 088922dfabca152047c8f39f660893b3b19dbe2d (patch) | |
| tree | 81e4b960e0417f4fc0fcb368fb093511f26670e7 /src/config/model.rs | |
| parent | d7ae163f8888cd9bf13e5a34e4d37f9518f76c01 (diff) | |
fix: let user keybinds displace defaults
refs #747
Diffstat (limited to 'src/config/model.rs')
| -rw-r--r-- | src/config/model.rs | 342 |
1 files changed, 337 insertions, 5 deletions
diff --git a/src/config/model.rs b/src/config/model.rs index 5396e26b..51af278f 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -1,11 +1,12 @@ -use std::num::NonZeroUsize; +use std::{collections::BTreeSet, num::NonZeroUsize}; use crossterm::event::KeyModifiers; use serde::{de, Deserialize, Deserializer, Serialize}; use super::{ - BindingConfig, CommandKeybindConfig, SoundConfig, ThemeConfig, DEFAULT_MOBILE_WIDTH_THRESHOLD, - DEFAULT_MOUSE_SCROLL_LINES, DEFAULT_SCROLLBACK_LIMIT_BYTES, + ActionKeybinds, BindingConfig, CommandKeybindConfig, IndexedKeybind, Keybinds, SoundConfig, + ThemeConfig, DEFAULT_MOBILE_WIDTH_THRESHOLD, DEFAULT_MOUSE_SCROLL_LINES, + DEFAULT_SCROLLBACK_LIMIT_BYTES, }; pub const MAX_TOAST_DELAY_SECONDS: u64 = 3600; @@ -287,8 +288,7 @@ pub struct LoadedConfig { pub invalid_sections: Vec<String>, } -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(default)] +#[derive(Debug, Clone, Serialize)] pub struct KeysConfig { /// Prefix key to enter prefix mode (e.g. "ctrl+b", "f12", "esc"). pub prefix: String, @@ -402,6 +402,337 @@ pub struct KeysConfig { /// Prefix-mode custom command bindings. #[serde(skip_serializing_if = "Vec::is_empty")] pub command: Vec<CommandKeybindConfig>, + #[serde(skip_serializing)] + pub(crate) user_fields: BTreeSet<&'static str>, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(default)] +pub(crate) struct KeysConfigOverlay { + #[serde(skip_serializing_if = "Option::is_none")] + prefix: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + help: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + settings: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + new_workspace: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + new_worktree: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + open_worktree: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + remove_worktree: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + rename_workspace: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + close_workspace: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + workspace_picker: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + goto: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + navigate_workspace_up: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + navigate_workspace_down: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + navigate_pane_left: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + navigate_pane_down: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + navigate_pane_up: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + navigate_pane_right: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + detach: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + reload_config: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + open_notification_target: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + previous_workspace: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + next_workspace: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + previous_agent: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + next_agent: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + focus_agent: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + remote_image_paste: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + new_tab: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + rename_tab: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + previous_tab: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + next_tab: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + switch_tab: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + switch_workspace: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + close_tab: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + rename_pane: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + edit_scrollback: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + copy_mode: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + focus_pane_left: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + focus_pane_down: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + focus_pane_up: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + focus_pane_right: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + swap_pane_left: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + swap_pane_down: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + swap_pane_up: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + swap_pane_right: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + cycle_pane_next: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + cycle_pane_previous: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + last_pane: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + split_vertical: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + split_horizontal: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + close_pane: Option<BindingConfig>, + #[serde(alias = "fullscreen", skip_serializing_if = "Option::is_none")] + zoom: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + resize_mode: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + toggle_sidebar: Option<BindingConfig>, + #[serde(skip_serializing_if = "Option::is_none")] + indexed: Option<IndexedKeysConfig>, + #[serde(skip_serializing)] + command: Option<Vec<CommandKeybindConfig>>, +} + +impl<'de> Deserialize<'de> for KeysConfig { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let input = KeysConfigOverlay::deserialize(deserializer)?; + let mut keys = KeysConfig::default(); + + macro_rules! apply_field { + ($field:ident) => { + if let Some(value) = input.$field { + keys.$field = value; + keys.user_fields.insert(stringify!($field)); + } + }; + } + + apply_field!(prefix); + apply_field!(help); + apply_field!(settings); + apply_field!(new_workspace); + apply_field!(new_worktree); + apply_field!(open_worktree); + apply_field!(remove_worktree); + apply_field!(rename_workspace); + apply_field!(close_workspace); + apply_field!(workspace_picker); + apply_field!(goto); + apply_field!(navigate_workspace_up); + apply_field!(navigate_workspace_down); + apply_field!(navigate_pane_left); + apply_field!(navigate_pane_down); + apply_field!(navigate_pane_up); + apply_field!(navigate_pane_right); + apply_field!(detach); + apply_field!(reload_config); + apply_field!(open_notification_target); + apply_field!(previous_workspace); + apply_field!(next_workspace); + apply_field!(previous_agent); + apply_field!(next_agent); + apply_field!(focus_agent); + apply_field!(remote_image_paste); + apply_field!(new_tab); + apply_field!(rename_tab); + apply_field!(previous_tab); + apply_field!(next_tab); + apply_field!(switch_tab); + apply_field!(switch_workspace); + apply_field!(close_tab); + apply_field!(rename_pane); + apply_field!(edit_scrollback); + apply_field!(copy_mode); + apply_field!(focus_pane_left); + apply_field!(focus_pane_down); + apply_field!(focus_pane_up); + apply_field!(focus_pane_right); + apply_field!(swap_pane_left); + apply_field!(swap_pane_down); + apply_field!(swap_pane_up); + apply_field!(swap_pane_right); + apply_field!(cycle_pane_next); + apply_field!(cycle_pane_previous); + apply_field!(last_pane); + apply_field!(split_vertical); + apply_field!(split_horizontal); + apply_field!(close_pane); + apply_field!(zoom); + apply_field!(resize_mode); + apply_field!(toggle_sidebar); + apply_field!(indexed); + apply_field!(command); + + Ok(keys) + } +} + +impl KeysConfig { + pub(crate) fn key_field_is_user_configured(&self, field: &str) -> bool { + self.user_fields.contains(field) + } + + pub(crate) fn local_profile(&self, keybinds: &Keybinds) -> KeysConfigOverlay { + let mut profile = KeysConfigOverlay::default(); + + macro_rules! copy_user_field { + ($field:ident) => { + if self.user_fields.contains(stringify!($field)) { + profile.$field = Some(self.$field.clone()); + } + }; + } + macro_rules! copy_effective_action_field { + ($field:ident, $target:expr) => { + if self.user_fields.contains(stringify!($field)) { + profile.$field = Some(self.$field.clone()); + } else if binding_config_is_effective(&self.$field, &$target) { + profile.$field = Some(self.$field.clone()); + } else if binding_config_has_values(&self.$field) { + profile.$field = Some(BindingConfig::empty()); + } + }; + } + macro_rules! copy_effective_indexed_field { + ($field:ident, $target:expr) => { + if self.user_fields.contains(stringify!($field)) { + profile.$field = Some(self.$field.clone()); + } else if let Some(effective) = effective_indexed_config(&self.$field, &$target) { + profile.$field = Some(effective); + } else if binding_config_has_values(&self.$field) { + profile.$field = Some(BindingConfig::empty()); + } + }; + } + + profile.prefix = Some(self.prefix.clone()); + copy_effective_action_field!(help, keybinds.help); + copy_effective_action_field!(settings, keybinds.settings); + copy_effective_action_field!(new_workspace, keybinds.new_workspace); + copy_effective_action_field!(new_worktree, keybinds.new_worktree); + copy_effective_action_field!(open_worktree, keybinds.open_worktree); + copy_effective_action_field!(remove_worktree, keybinds.remove_worktree); + copy_effective_action_field!(rename_workspace, keybinds.rename_workspace); + copy_effective_action_field!(close_workspace, keybinds.close_workspace); + copy_effective_action_field!(workspace_picker, keybinds.workspace_picker); + copy_effective_action_field!(goto, keybinds.goto); + copy_effective_action_field!(navigate_workspace_up, keybinds.navigate.workspace_up); + copy_effective_action_field!(navigate_workspace_down, keybinds.navigate.workspace_down); + copy_effective_action_field!(navigate_pane_left, keybinds.navigate.pane_left); + copy_effective_action_field!(navigate_pane_down, keybinds.navigate.pane_down); + copy_effective_action_field!(navigate_pane_up, keybinds.navigate.pane_up); + copy_effective_action_field!(navigate_pane_right, keybinds.navigate.pane_right); + copy_effective_action_field!(detach, keybinds.detach); + copy_effective_action_field!(reload_config, keybinds.reload_config); + copy_effective_action_field!(open_notification_target, keybinds.open_notification_target); + copy_effective_action_field!(previous_workspace, keybinds.previous_workspace); + copy_effective_action_field!(next_workspace, keybinds.next_workspace); + copy_effective_action_field!(previous_agent, keybinds.previous_agent); + copy_effective_action_field!(next_agent, keybinds.next_agent); + copy_effective_indexed_field!(focus_agent, keybinds.focus_agent); + copy_user_field!(remote_image_paste); + copy_effective_action_field!(new_tab, keybinds.new_tab); + copy_effective_action_field!(rename_tab, keybinds.rename_tab); + copy_effective_action_field!(previous_tab, keybinds.previous_tab); + copy_effective_action_field!(next_tab, keybinds.next_tab); + copy_effective_indexed_field!(switch_tab, keybinds.switch_tab); + copy_effective_indexed_field!(switch_workspace, keybinds.switch_workspace); + copy_effective_action_field!(close_tab, keybinds.close_tab); + copy_effective_action_field!(rename_pane, keybinds.rename_pane); + copy_effective_action_field!(edit_scrollback, keybinds.edit_scrollback); + copy_effective_action_field!(copy_mode, keybinds.copy_mode); + copy_effective_action_field!(focus_pane_left, keybinds.focus_pane_left); + copy_effective_action_field!(focus_pane_down, keybinds.focus_pane_down); + copy_effective_action_field!(focus_pane_up, keybinds.focus_pane_up); + copy_effective_action_field!(focus_pane_right, keybinds.focus_pane_right); + copy_effective_action_field!(swap_pane_left, keybinds.swap_pane_left); + copy_effective_action_field!(swap_pane_down, keybinds.swap_pane_down); + copy_effective_action_field!(swap_pane_up, keybinds.swap_pane_up); + copy_effective_action_field!(swap_pane_right, keybinds.swap_pane_right); + copy_effective_action_field!(cycle_pane_next, keybinds.cycle_pane_next); + copy_effective_action_field!(cycle_pane_previous, keybinds.cycle_pane_previous); + copy_effective_action_field!(last_pane, keybinds.last_pane); + copy_effective_action_field!(split_vertical, keybinds.split_vertical); + copy_effective_action_field!(split_horizontal, keybinds.split_horizontal); + copy_effective_action_field!(close_pane, keybinds.close_pane); + copy_effective_action_field!(zoom, keybinds.zoom); + copy_effective_action_field!(resize_mode, keybinds.resize_mode); + copy_effective_action_field!(toggle_sidebar, keybinds.toggle_sidebar); + copy_user_field!(indexed); + + profile + } +} + +fn binding_config_has_values(config: &BindingConfig) -> bool { + config.has_values() +} + +fn binding_config_is_effective(config: &BindingConfig, keybinds: &ActionKeybinds) -> bool { + !binding_config_has_values(config) || !keybinds.bindings.is_empty() +} + +fn effective_indexed_config( + config: &BindingConfig, + keybinds: &[IndexedKeybind], +) -> Option<BindingConfig> { + if !binding_config_has_values(config) { + return Some(config.clone()); + } + + let expected_labels = config.indexed_labels(); + if expected_labels.is_empty() { + return None; + } + + let effective_labels: Vec<String> = expected_labels + .iter() + .filter(|expected| { + keybinds + .iter() + .any(|binding| binding.label.as_str() == expected.as_str()) + }) + .cloned() + .collect(); + + if effective_labels.is_empty() { + None + } else if effective_labels.len() == expected_labels.len() { + Some(config.clone()) + } else { + Some(BindingConfig::Many(effective_labels)) + } } #[derive(Debug, Default, Clone, Deserialize, Serialize)] @@ -611,6 +942,7 @@ impl Default for KeysConfig { toggle_sidebar: BindingConfig::one("prefix+b"), indexed: IndexedKeysConfig::default(), command: Vec::new(), + user_fields: BTreeSet::new(), } } } |
