diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-26 02:07:33 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-26 02:07:33 +0300 |
| commit | 3d1ef155bf9846f39725bf22c54666da2bc42330 (patch) | |
| tree | f4b4c3eca6c6f821dbfe86bcee0fd6a5d9ab9da9 /src/app/mod.rs | |
| parent | ef4182efd9f15e6c13d91d45e525b094cdcee2fe (diff) | |
feat: add live config reload
Diffstat (limited to 'src/app/mod.rs')
| -rw-r--r-- | src/app/mod.rs | 203 |
1 files changed, 176 insertions, 27 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index 14c50f7f..c4806a77 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -247,7 +247,7 @@ impl App { detach_requested: false, request_new_workspace: false, request_new_tab: false, - request_reload_keybinds: false, + request_reload_config: false, request_clipboard_write: None, creating_new_tab: false, requested_new_tab_name: None, @@ -299,6 +299,7 @@ impl App { pane_scrollback_limit_bytes: config.advanced.scrollback_limit_bytes, accent: crate::config::parse_color(&config.ui.accent), sound: config.ui.sound.clone(), + local_sound_playback: true, toast_config: config.ui.toast.clone(), keybinds: config.keybinds(), spinner_tick: 0, @@ -415,9 +416,9 @@ impl App { needs_render = true; } - if self.state.request_reload_keybinds { - self.state.request_reload_keybinds = false; - self.reload_keybinds(); + if self.state.request_reload_config { + self.state.request_reload_config = false; + self.reload_config(); needs_render = true; } @@ -531,32 +532,105 @@ impl App { crate::app::input::open_settings(&mut self.state); } - pub(crate) fn reload_keybinds(&mut self) { + pub(crate) fn reload_config(&mut self) -> crate::config::ConfigReloadReport { + self.apply_config_from_disk(true) + } + + pub(crate) fn apply_config_from_disk( + &mut self, + notify_success: bool, + ) -> crate::config::ConfigReloadReport { let previous_toast = self.state.toast.clone(); - match crate::config::load_live_keybinds() { + let report = match crate::config::load_live_config() { + Ok(loaded) => self.apply_live_config(&loaded.config, notify_success), + Err(diagnostics) => { + self.state.toast = None; + self.state.config_diagnostic = + crate::config::config_diagnostic_summary(&diagnostics); + self.config_diagnostic_deadline = Some(Instant::now() + Duration::from_secs(8)); + crate::config::ConfigReloadReport { + status: crate::config::ConfigReloadStatus::Failed, + diagnostics, + } + } + }; + self.sync_toast_deadline(previous_toast); + report + } + + fn apply_live_config( + &mut self, + config: &crate::config::Config, + notify_success: bool, + ) -> crate::config::ConfigReloadReport { + let mut diagnostics = Vec::new(); + + match config.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; + } + Err(keybind_diagnostics) => { + let mut message = keybind_diagnostics.join("; "); + if !message.contains("keeping current keybinds") { + message.push_str("; keeping current keybinds"); + } + diagnostics.push(message); + } + } + + diagnostics.extend(config.ui.sound.diagnostics()); + + let previous_default_sidebar_width = self.state.default_sidebar_width; + self.state.default_sidebar_width = config.ui.sidebar_width; + if self.state.sidebar_width == previous_default_sidebar_width { + self.state.sidebar_width = config.ui.sidebar_width; + } + self.state.confirm_close = config.ui.confirm_close; + self.state.pane_scrollback_limit_bytes = config.advanced.scrollback_limit_bytes; + self.state.accent = crate::config::parse_color(&config.ui.accent); + self.state.sound = config.ui.sound.clone(); + self.state.toast_config = config.ui.toast.clone(); + self.state.palette = resolve_palette(config); + self.state.theme_name = config + .theme + .name + .clone() + .unwrap_or_else(|| "catppuccin".to_string()); + + let status = if diagnostics.is_empty() { + crate::config::ConfigReloadStatus::Applied + } else { + crate::config::ConfigReloadStatus::Partial + }; + + if diagnostics.is_empty() { + self.state.config_diagnostic = None; + self.config_diagnostic_deadline = None; + if notify_success { self.state.toast = Some(crate::app::state::ToastNotification { kind: crate::app::state::ToastKind::UpdateInstalled, - title: "reloaded keybinds".to_string(), + title: "reloaded config".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)); + } else { + self.state.config_diagnostic = crate::config::config_diagnostic_summary(&diagnostics); + self.config_diagnostic_deadline = Some(Instant::now() + Duration::from_secs(8)); + if notify_success { + self.state.toast = Some(crate::app::state::ToastNotification { + kind: crate::app::state::ToastKind::UpdateInstalled, + title: "reloaded config".to_string(), + context: "with warnings".to_string(), + }); } } - self.sync_toast_deadline(previous_toast); + + crate::config::ConfigReloadReport { + status, + diagnostics, + } } } @@ -794,30 +868,35 @@ mod tests { } #[test] - fn reload_keybinds_updates_live_state() { + fn reload_config_updates_live_state() { let _guard = config_env_lock().lock().unwrap(); - let path = temp_config_path("reload-keybinds-success"); + let path = temp_config_path("reload-config-success"); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write( &path, - "[keys]\nnew_workspace = \"g\"\nprefix = \"ctrl+a\"\n", + "[keys]\nnew_workspace = \"g\"\nprefix = \"ctrl+a\"\n[ui.toast]\ndelivery = \"herdr\"\n", ) .unwrap(); std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); let mut app = test_app(); - app.reload_keybinds(); + let report = app.reload_config(); + assert_eq!(report.status, crate::config::ConfigReloadStatus::Applied); 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_eq!( + app.state.toast_config.delivery, + crate::config::ToastDelivery::Herdr + ); 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.title, "reloaded config"); assert_eq!(toast.context, "using config.toml"); std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); @@ -825,23 +904,32 @@ mod tests { } #[test] - fn reload_keybinds_keeps_current_state_on_invalid_binding() { + fn reload_config_keeps_current_keybinds_on_invalid_binding_but_applies_other_sections() { let _guard = config_env_lock().lock().unwrap(); - let path = temp_config_path("reload-keybinds-invalid"); + let path = temp_config_path("reload-config-invalid-keybind"); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - std::fs::write(&path, "[keys]\nnew_workspace = \"wat\"\n").unwrap(); + std::fs::write( + &path, + "[keys]\nnew_workspace = \"wat\"\n[ui.toast]\ndelivery = \"terminal\"\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(); + let report = app.reload_config(); + assert_eq!(report.status, crate::config::ConfigReloadStatus::Partial); assert_eq!( (app.state.prefix_code, app.state.prefix_mods), original_prefix ); assert_eq!(app.state.keybinds.new_workspace, original_keybinds); + assert_eq!( + app.state.toast_config.delivery, + crate::config::ToastDelivery::Terminal + ); assert!(app .state .config_diagnostic @@ -850,6 +938,67 @@ mod tests { message.contains("keys.new_workspace") && message.contains("keeping current keybinds") })); + + std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } + + #[test] + fn settings_save_toast_delivery_persists_then_applies_live_config() { + let _guard = config_env_lock().lock().unwrap(); + let path = temp_config_path("settings-save-toast-delivery"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, "onboarding = false\n").unwrap(); + std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); + + let mut app = test_app(); + assert_eq!( + app.state.toast_config.delivery, + crate::config::ToastDelivery::Off + ); + + app.save_toast_delivery(crate::config::ToastDelivery::Terminal); + + assert_eq!( + app.state.toast_config.delivery, + crate::config::ToastDelivery::Terminal + ); + let content = std::fs::read_to_string(&path).unwrap(); + assert!(content.contains("delivery = \"terminal\"")); + assert!(app.state.config_diagnostic.is_none()); + + std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } + + #[test] + fn reload_config_keeps_current_state_on_invalid_toml() { + let _guard = config_env_lock().lock().unwrap(); + let path = temp_config_path("reload-config-invalid-toml"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, "[keys\nnew_workspace = \"g\"\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; + let original_toast_delivery = app.state.toast_config.delivery; + let report = app.reload_config(); + + assert_eq!(report.status, crate::config::ConfigReloadStatus::Failed); + assert_eq!( + (app.state.prefix_code, app.state.prefix_mods), + original_prefix + ); + assert_eq!(app.state.keybinds.new_workspace, original_keybinds); + assert_eq!(app.state.toast_config.delivery, original_toast_delivery); + assert!(app + .state + .config_diagnostic + .as_deref() + .is_some_and(|message| { + message.contains("config parse error") && message.contains("keeping current config") + })); assert!(app.state.toast.is_none()); std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); |
