aboutsummaryrefslogtreecommitdiff
path: root/src/app/mod.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-26 02:07:33 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-26 02:07:33 +0300
commit3d1ef155bf9846f39725bf22c54666da2bc42330 (patch)
treef4b4c3eca6c6f821dbfe86bcee0fd6a5d9ab9da9 /src/app/mod.rs
parentef4182efd9f15e6c13d91d45e525b094cdcee2fe (diff)
feat: add live config reload
Diffstat (limited to 'src/app/mod.rs')
-rw-r--r--src/app/mod.rs203
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);