use std::any::Any;
use crossterm::event::KeyEvent;
use ratatui::layout::Rect;
use ratatui::Frame;
use super::types::{Modal, ModalOutcome, ModalRenderCtx};
use crate::app::{App, MessageKind};
use crate::config;
use crate::config::{Config, KeyBindingOverrides, KeyMap};
use crate::ui::{KeybindsResponse, KeybindsState, KeybindsView, ModalKind};
pub struct KeybindsOverlayModal {
state: KeybindsState,
}
impl KeybindsOverlayModal {
pub fn new(keymap: &KeyMap, overrides: &KeyBindingOverrides, vim_enabled: bool) -> Self {
Self {
state: KeybindsState::open(keymap, overrides, vim_enabled),
}
}
}
fn try_persist(overrides: &KeyBindingOverrides) -> Result<(), String> {
if !config::config_writes_allowed() {
return Ok(());
}
let Some(dir) = Config::config_dir() else {
return Err("No config directory available — keybindings not saved".into());
};
let path = dir.join("keybindings.toml");
overrides.save_to(&path).map_err(|e| {
tracing::warn!(error = %e, "failed to write keybindings.toml");
format!("Failed to save keybindings: {e}")
})
}
fn install_drafts(app: &mut App, keymap: KeyMap, overrides: KeyBindingOverrides) {
app.keymap = Some(keymap);
app.keybindings = overrides;
let msg = if config::config_writes_allowed() {
"Keybindings saved".to_owned()
} else {
format!("Keybindings applied{}", config::NOT_PERSISTED_NOTE)
};
app.flash(msg, MessageKind::Success);
}
fn outcome_for_save(
keymap: KeyMap,
overrides: KeyBindingOverrides,
notify_inline: Option<&mut App>,
) -> ModalOutcome {
match try_persist(&overrides) {
Ok(()) => {
ModalOutcome::CloseAnd(Box::new(move |app| install_drafts(app, keymap, overrides)))
}
Err(msg) => match notify_inline {
Some(app) => {
app.notify(msg, ModalKind::Warning);
ModalOutcome::Continue
}
None => ModalOutcome::ContinueAnd(Box::new(move |app| {
app.notify(msg, ModalKind::Warning);
})),
},
}
}
impl Modal for KeybindsOverlayModal {
fn render(&mut self, frame: &mut Frame<'_>, area: Rect, ctx: &ModalRenderCtx<'_>) {
let view = KeybindsView { theme: ctx.theme };
frame.render_stateful_widget(view, area, &mut self.state);
}
fn handle_key(
&mut self,
key: KeyEvent,
app: &mut App,
_doc_height: usize,
_doc_width: usize,
) -> ModalOutcome {
match self.state.handle_key(&key) {
KeybindsResponse::Continue => ModalOutcome::Continue,
KeybindsResponse::Cancelled => ModalOutcome::Close,
KeybindsResponse::Save { keymap, overrides } => {
outcome_for_save(keymap, overrides, Some(app))
}
}
}
fn handle_wheel(&mut self, delta: i32) {
self.state.scroll_state.scroll_by(delta);
}
fn handle_click(&mut self, col: u16, row: u16, _app: &mut App) -> ModalOutcome {
match self.state.handle_click(col, row) {
KeybindsResponse::Continue => ModalOutcome::Continue,
KeybindsResponse::Cancelled => ModalOutcome::Close,
KeybindsResponse::Save { keymap, overrides } => {
outcome_for_save(keymap, overrides, None)
}
}
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::test_utils::make_app;
use crate::config::Action;
#[test]
fn open_keybinds_action_opens_combined_keybinds_overlay() {
let mut app = make_app();
let handled = app.handle_app_action(&Action::OpenKeybinds, 40, 80);
assert!(handled);
assert!(app.modal_stack.contains::<KeybindsOverlayModal>());
}
#[test]
fn saving_writes_no_keybindings_file_while_writes_are_suppressed() {
let _lock = crate::test_env::env_lock();
let dir = tempfile::tempdir().unwrap();
let _xdg = crate::test_env::EnvGuard::set("XDG_CONFIG_HOME", dir.path());
let path = dir.path().join("edamame/keybindings.toml");
let overrides = KeyBindingOverrides(
[("Quit".to_owned(), "ctrl+x".to_owned())]
.into_iter()
.collect(),
);
{
let _suppressed = crate::config::persistence::SuppressGuard::new();
assert_eq!(
try_persist(&overrides),
Ok(()),
"suppression is not a failure"
);
assert!(!path.exists(), "--no-config must not create {path:?}");
}
assert_eq!(try_persist(&overrides), Ok(()));
assert!(path.exists());
assert!(std::fs::read_to_string(&path).unwrap().contains("ctrl+x"));
}
#[test]
fn the_save_flash_says_so_when_nothing_was_written() {
let _lock = crate::test_env::env_lock();
let mut app = make_app();
let keymap = app.ensure_keymap_clone();
{
let _suppressed = crate::config::persistence::SuppressGuard::new();
install_drafts(&mut app, keymap.clone(), KeyBindingOverrides::default());
let msg = app.transient.as_ref().expect("flash").text.clone();
assert!(msg.contains(crate::config::NOT_PERSISTED_NOTE), "{msg}");
assert!(msg.starts_with("Keybindings applied"), "{msg}");
}
install_drafts(&mut app, keymap, KeyBindingOverrides::default());
assert_eq!(
app.transient.as_ref().expect("flash").text,
"Keybindings saved"
);
}
}