aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-09 04:06:44 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-09 04:07:14 +0300
commitb5b61c14841fda60f289cfd9c782580ea099a2a2 (patch)
treef38fcf143d5dd99e3bc1540ac1f8f9ca83468031 /src/app
parenta3ecb4e3b4151e840ad237e073d8282e1181c34a (diff)
parent74a6161bff998c311d0d43bf6c44de5e193c23e6 (diff)
merge: reload keybinds
Diffstat (limited to 'src/app')
-rw-r--r--src/app/input.rs46
-rw-r--r--src/app/mod.rs116
-rw-r--r--src/app/state.rs2
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,