diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-09 04:06:44 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-09 04:07:14 +0300 |
| commit | b5b61c14841fda60f289cfd9c782580ea099a2a2 (patch) | |
| tree | f38fcf143d5dd99e3bc1540ac1f8f9ca83468031 /src/app | |
| parent | a3ecb4e3b4151e840ad237e073d8282e1181c34a (diff) | |
| parent | 74a6161bff998c311d0d43bf6c44de5e193c23e6 (diff) | |
merge: reload keybinds
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/input.rs | 46 | ||||
| -rw-r--r-- | src/app/mod.rs | 116 | ||||
| -rw-r--r-- | src/app/state.rs | 2 |
3 files changed, 159 insertions, 5 deletions
diff --git a/src/app/input.rs b/src/app/input.rs index c7874bbc..ada8e6d9 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -478,11 +478,16 @@ enum GlobalMenuAction { ApplyUpdate, WhatsNew, Keybinds, + ReloadKeybinds, Settings, } fn global_menu_actions(state: &AppState) -> Vec<GlobalMenuAction> { - let mut actions = vec![GlobalMenuAction::Settings, GlobalMenuAction::Keybinds]; + let mut actions = vec![ + GlobalMenuAction::Settings, + GlobalMenuAction::Keybinds, + GlobalMenuAction::ReloadKeybinds, + ]; if state.latest_release_notes_available || state.update_available.is_some() { actions.push(GlobalMenuAction::WhatsNew); } @@ -652,6 +657,10 @@ fn apply_global_menu_action(state: &mut AppState, action: GlobalMenuAction) { GlobalMenuAction::ApplyUpdate => state.should_quit = true, GlobalMenuAction::WhatsNew => open_update_release_notes(state), GlobalMenuAction::Keybinds => open_keybind_help(state), + GlobalMenuAction::ReloadKeybinds => { + state.request_reload_keybinds = true; + leave_modal(state); + } GlobalMenuAction::Settings => open_settings(state), } } @@ -1594,7 +1603,7 @@ impl AppState { } pub(crate) fn global_menu_labels(&self) -> Vec<&'static str> { - let mut labels = vec!["settings", "keybinds"]; + let mut labels = vec!["settings", "keybinds", "reload keybinds"]; if self.latest_release_notes_available || self.update_available.is_some() { labels.push("what's new"); } @@ -3509,6 +3518,27 @@ mod tests { } #[test] + fn clicking_reload_keybinds_menu_item_requests_reload() { + let mut app = app_for_mouse_test(); + let launcher = app.state.global_launcher_rect(); + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + launcher.x, + launcher.y, + )); + + let menu = app.state.global_menu_rect(); + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + menu.x + 2, + menu.y + 3, + )); + + assert!(app.state.request_reload_keybinds); + assert_eq!(app.state.mode, Mode::Navigate); + } + + #[test] fn update_pending_menu_surfaces_apply_update_action() { let mut app = app_for_mouse_test(); app.state.update_available = Some("0.3.2".into()); @@ -3523,14 +3553,20 @@ mod tests { assert_eq!( app.state.global_menu_labels(), - vec!["settings", "keybinds", "what's new", "quit to apply update"] + vec![ + "settings", + "keybinds", + "reload keybinds", + "what's new", + "quit to apply update" + ] ); let menu = app.state.global_menu_rect(); app.handle_mouse(mouse( MouseEventKind::Down(MouseButton::Left), menu.x + 2, - menu.y + 4, + menu.y + 5, )); assert!(app.state.should_quit); @@ -3543,7 +3579,7 @@ mod tests { assert_eq!( app.state.global_menu_labels(), - vec!["settings", "keybinds", "what's new"] + vec!["settings", "keybinds", "reload keybinds", "what's new"] ); } diff --git a/src/app/mod.rs b/src/app/mod.rs index 87af63e5..a39ed436 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -238,6 +238,7 @@ impl App { should_quit: false, request_new_workspace: false, request_new_tab: false, + request_reload_keybinds: false, creating_new_tab: false, requested_new_tab_name: None, request_complete_onboarding: false, @@ -435,6 +436,12 @@ impl App { needs_render = true; } + if self.state.request_reload_keybinds { + self.state.request_reload_keybinds = false; + self.reload_keybinds(); + needs_render = true; + } + let now = Instant::now(); self.sync_animation_timer(now); @@ -2034,6 +2041,34 @@ impl App { } } + fn reload_keybinds(&mut self) { + let previous_toast = self.state.toast.clone(); + match crate::config::load_live_keybinds() { + Ok(live) => { + self.state.prefix_code = live.prefix.0; + self.state.prefix_mods = live.prefix.1; + self.state.keybinds = live.keybinds; + self.state.config_diagnostic = None; + self.config_diagnostic_deadline = None; + self.state.toast = Some(crate::app::state::ToastNotification { + kind: crate::app::state::ToastKind::UpdateInstalled, + title: "reloaded keybinds".to_string(), + context: "using config.toml".to_string(), + }); + } + Err(diagnostics) => { + let mut message = diagnostics.join("; "); + if !message.contains("keeping current keybinds") { + message.push_str("; keeping current keybinds"); + } + self.state.toast = None; + self.state.config_diagnostic = Some(message); + self.config_diagnostic_deadline = Some(Instant::now() + Duration::from_secs(8)); + } + } + self.sync_toast_deadline(previous_toast); + } + fn save_theme(&mut self, name: &str) { self.update_config_file("theme", |content| { crate::config::upsert_section_value(content, "theme", "name", &format!("\"{name}\"")) @@ -2410,6 +2445,7 @@ mod tests { use crate::detect::{Agent, AgentState}; use crate::workspace::Workspace; use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers}; + use std::sync::{Mutex, OnceLock}; fn raw_key( code: KeyCode, @@ -2442,6 +2478,86 @@ mod tests { ) } + fn config_env_lock() -> &'static Mutex<()> { + static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } + + fn temp_config_path(name: &str) -> std::path::PathBuf { + let unique = format!( + "herdr-{name}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + std::env::temp_dir().join(unique).join("config.toml") + } + + #[test] + fn reload_keybinds_updates_live_state() { + let _guard = config_env_lock().lock().unwrap(); + let path = temp_config_path("reload-keybinds-success"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write( + &path, + "[keys]\nnew_workspace = \"g\"\nprefix = \"ctrl+a\"\n", + ) + .unwrap(); + std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); + + let mut app = test_app(); + app.reload_keybinds(); + + assert_eq!(app.state.prefix_code, KeyCode::Char('a')); + assert_eq!(app.state.prefix_mods, KeyModifiers::CONTROL); + assert_eq!( + app.state.keybinds.new_workspace, + (KeyCode::Char('g'), KeyModifiers::empty()) + ); + assert!(app.state.config_diagnostic.is_none()); + let toast = app.state.toast.as_ref().unwrap(); + assert_eq!(toast.kind, crate::app::state::ToastKind::UpdateInstalled); + assert_eq!(toast.title, "reloaded keybinds"); + assert_eq!(toast.context, "using config.toml"); + + std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } + + #[test] + fn reload_keybinds_keeps_current_state_on_invalid_binding() { + let _guard = config_env_lock().lock().unwrap(); + let path = temp_config_path("reload-keybinds-invalid"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, "[keys]\nnew_workspace = \"wat\"\n").unwrap(); + std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); + + let mut app = test_app(); + let original_prefix = (app.state.prefix_code, app.state.prefix_mods); + let original_keybinds = app.state.keybinds.new_workspace; + app.reload_keybinds(); + + assert_eq!( + (app.state.prefix_code, app.state.prefix_mods), + original_prefix + ); + assert_eq!(app.state.keybinds.new_workspace, original_keybinds); + assert!(app + .state + .config_diagnostic + .as_deref() + .is_some_and(|message| { + message.contains("keys.new_workspace") + && message.contains("keeping current keybinds") + })); + assert!(app.state.toast.is_none()); + + std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } + #[tokio::test] async fn raw_input_waits_when_reader_is_gone() { let result = diff --git a/src/app/state.rs b/src/app/state.rs index ea388daa..008d45c8 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -574,6 +574,7 @@ pub struct AppState { pub should_quit: bool, pub request_new_workspace: bool, pub request_new_tab: bool, + pub request_reload_keybinds: bool, pub creating_new_tab: bool, pub requested_new_tab_name: Option<String>, pub request_complete_onboarding: bool, @@ -687,6 +688,7 @@ impl AppState { should_quit: false, request_new_workspace: false, request_new_tab: false, + request_reload_keybinds: false, creating_new_tab: false, requested_new_tab_name: None, request_complete_onboarding: false, |
