aboutsummaryrefslogtreecommitdiff
path: root/src/app/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/mod.rs')
-rw-r--r--src/app/mod.rs266
1 files changed, 191 insertions, 75 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs
index c4806a77..26090ebf 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -116,6 +116,13 @@ fn auto_updates_enabled(no_session: bool) -> bool {
/// Resolve the palette from config: base theme + optional custom overrides.
fn resolve_palette(config: &crate::config::Config) -> state::Palette {
+ resolve_palette_with_legacy_accent(config, true)
+}
+
+fn resolve_palette_with_legacy_accent(
+ config: &crate::config::Config,
+ use_legacy_ui_accent: bool,
+) -> state::Palette {
// Start with the named theme (default: catppuccin)
let base_name = config.theme.name.as_deref().unwrap_or("catppuccin");
let mut palette = state::Palette::from_name(base_name).unwrap_or_else(|| {
@@ -132,7 +139,8 @@ fn resolve_palette(config: &crate::config::Config) -> state::Palette {
}
// Legacy: if ui.accent is set and no theme.custom.accent, use it for compat
- if config.ui.accent != "cyan"
+ if use_legacy_ui_accent
+ && config.ui.accent != "cyan"
&& config
.theme
.custom
@@ -161,59 +169,78 @@ impl App {
let render_dirty = Arc::new(AtomicBool::new(false));
// Try to restore previous session
- let (workspaces, active, selected, agent_panel_scope, sidebar_width, sidebar_section_split) =
- if no_session {
+ let (
+ workspaces,
+ active,
+ selected,
+ agent_panel_scope,
+ sidebar_width,
+ sidebar_width_source,
+ sidebar_section_split,
+ ) = if no_session {
+ (
+ Vec::new(),
+ None,
+ 0,
+ state::AgentPanelScope::CurrentWorkspace,
+ config.ui.sidebar_width,
+ state::SidebarWidthSource::ConfigDefault,
+ 0.5_f32,
+ )
+ } else if let Some(snap) = crate::persist::load() {
+ let ws = crate::persist::restore(
+ &snap,
+ 24,
+ 80,
+ config.advanced.scrollback_limit_bytes,
+ event_tx.clone(),
+ render_notify.clone(),
+ render_dirty.clone(),
+ );
+ if ws.is_empty() {
+ crate::logging::session_restored(0, "empty");
(
Vec::new(),
None,
0,
- state::AgentPanelScope::CurrentWorkspace,
- config.ui.sidebar_width,
- 0.5_f32,
+ snap.agent_panel_scope,
+ snap.sidebar_width.unwrap_or(config.ui.sidebar_width),
+ if snap.sidebar_width.is_some() {
+ state::SidebarWidthSource::Persisted
+ } else {
+ state::SidebarWidthSource::ConfigDefault
+ },
+ snap.sidebar_section_split.unwrap_or(0.5),
)
- } else if let Some(snap) = crate::persist::load() {
- let ws = crate::persist::restore(
- &snap,
- 24,
- 80,
- config.advanced.scrollback_limit_bytes,
- event_tx.clone(),
- render_notify.clone(),
- render_dirty.clone(),
- );
- if ws.is_empty() {
- crate::logging::session_restored(0, "empty");
- (
- Vec::new(),
- None,
- 0,
- snap.agent_panel_scope,
- snap.sidebar_width.unwrap_or(config.ui.sidebar_width),
- snap.sidebar_section_split.unwrap_or(0.5),
- )
- } else {
- crate::logging::session_restored(ws.len(), "ok");
- let active = snap.active.filter(|&i| i < ws.len());
- let selected = snap.selected.min(ws.len().saturating_sub(1));
- (
- ws,
- active,
- selected,
- snap.agent_panel_scope,
- snap.sidebar_width.unwrap_or(config.ui.sidebar_width),
- snap.sidebar_section_split.unwrap_or(0.5),
- )
- }
} else {
+ crate::logging::session_restored(ws.len(), "ok");
+ let active = snap.active.filter(|&i| i < ws.len());
+ let selected = snap.selected.min(ws.len().saturating_sub(1));
(
- Vec::new(),
- None,
- 0,
- state::AgentPanelScope::CurrentWorkspace,
- config.ui.sidebar_width,
- 0.5_f32,
+ ws,
+ active,
+ selected,
+ snap.agent_panel_scope,
+ snap.sidebar_width.unwrap_or(config.ui.sidebar_width),
+ if snap.sidebar_width.is_some() {
+ state::SidebarWidthSource::Persisted
+ } else {
+ state::SidebarWidthSource::ConfigDefault
+ },
+ snap.sidebar_section_split.unwrap_or(0.5),
)
- };
+ }
+ } else {
+ (
+ Vec::new(),
+ None,
+ 0,
+ state::AgentPanelScope::CurrentWorkspace,
+ config.ui.sidebar_width,
+ state::SidebarWidthSource::ConfigDefault,
+ 0.5_f32,
+ )
+ };
info!(
pane_scrollback_limit_bytes = config.advanced.scrollback_limit_bytes,
@@ -248,6 +275,7 @@ impl App {
request_new_workspace: false,
request_new_tab: false,
request_reload_config: false,
+ request_client_sound_config_reload: false,
request_clipboard_write: None,
creating_new_tab: false,
requested_new_tab_name: None,
@@ -291,6 +319,7 @@ impl App {
prefix_mods,
default_sidebar_width: config.ui.sidebar_width,
sidebar_width,
+ sidebar_width_source,
sidebar_width_auto: false,
sidebar_collapsed: false,
sidebar_section_split,
@@ -542,7 +571,12 @@ impl App {
) -> crate::config::ConfigReloadReport {
let previous_toast = self.state.toast.clone();
let report = match crate::config::load_live_config() {
- Ok(loaded) => self.apply_live_config(&loaded.config, notify_success),
+ Ok(loaded) => self.apply_live_config(
+ &loaded.config,
+ &loaded.diagnostics,
+ &loaded.invalid_sections,
+ notify_success,
+ ),
Err(diagnostics) => {
self.state.toast = None;
self.state.config_diagnostic =
@@ -561,43 +595,59 @@ impl App {
fn apply_live_config(
&mut self,
config: &crate::config::Config,
+ load_diagnostics: &[String],
+ invalid_sections: &[String],
notify_success: bool,
) -> crate::config::ConfigReloadReport {
- let mut diagnostics = Vec::new();
+ let mut diagnostics = load_diagnostics.to_vec();
+ let invalid_section =
+ |section: &str| invalid_sections.iter().any(|invalid| invalid == section);
+
+ if !invalid_section("keys") {
+ 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;
+ }
+ Err(keybind_diagnostics) => {
+ let mut message = keybind_diagnostics.join("; ");
+ if !message.contains("keeping current keybinds") {
+ message.push_str("; keeping current keybinds");
+ }
+ diagnostics.push(message);
+ }
+ }
+ }
+
+ if !invalid_section("ui") {
+ diagnostics.extend(config.ui.sound.diagnostics());
- 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.default_sidebar_width = config.ui.sidebar_width;
+ if self.state.sidebar_width_source == state::SidebarWidthSource::ConfigDefault {
+ self.state.sidebar_width = config.ui.sidebar_width;
}
- Err(keybind_diagnostics) => {
- let mut message = keybind_diagnostics.join("; ");
- if !message.contains("keeping current keybinds") {
- message.push_str("; keeping current keybinds");
- }
- diagnostics.push(message);
+ self.state.confirm_close = config.ui.confirm_close;
+ self.state.accent = crate::config::parse_color(&config.ui.accent);
+ if !self.state.local_sound_playback && self.state.sound != config.ui.sound {
+ self.state.request_client_sound_config_reload = true;
}
+ self.state.sound = config.ui.sound.clone();
+ self.state.toast_config = config.ui.toast.clone();
}
- diagnostics.extend(config.ui.sound.diagnostics());
+ if !invalid_section("advanced") {
+ self.state.pane_scrollback_limit_bytes = config.advanced.scrollback_limit_bytes;
+ }
- 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;
+ if !invalid_section("theme") {
+ self.state.palette = resolve_palette_with_legacy_accent(config, !invalid_section("ui"));
+ self.state.theme_name = config
+ .theme
+ .name
+ .clone()
+ .unwrap_or_else(|| "catppuccin".to_string());
}
- 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
@@ -904,6 +954,37 @@ mod tests {
}
#[test]
+ fn reload_config_updates_sidebar_width_only_when_config_owned() {
+ let _guard = config_env_lock().lock().unwrap();
+ let path = temp_config_path("reload-config-sidebar-width");
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path);
+
+ let mut app = test_app();
+ assert_eq!(
+ app.state.sidebar_width_source,
+ state::SidebarWidthSource::ConfigDefault
+ );
+
+ std::fs::write(&path, "[ui]\nsidebar_width = 42\n").unwrap();
+ let report = app.reload_config();
+ assert_eq!(report.status, crate::config::ConfigReloadStatus::Applied);
+ assert_eq!(app.state.default_sidebar_width, 42);
+ assert_eq!(app.state.sidebar_width, 42);
+
+ app.state.sidebar_width = 31;
+ app.state.sidebar_width_source = state::SidebarWidthSource::Manual;
+ std::fs::write(&path, "[ui]\nsidebar_width = 44\n").unwrap();
+ let report = app.reload_config();
+ assert_eq!(report.status, crate::config::ConfigReloadStatus::Applied);
+ assert_eq!(app.state.default_sidebar_width, 44);
+ assert_eq!(app.state.sidebar_width, 31);
+
+ 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_keybinds_on_invalid_binding_but_applies_other_sections() {
let _guard = config_env_lock().lock().unwrap();
let path = temp_config_path("reload-config-invalid-keybind");
@@ -944,6 +1025,41 @@ mod tests {
}
#[test]
+ fn reload_config_preserves_invalid_ui_section_but_applies_valid_keys() {
+ let _guard = config_env_lock().lock().unwrap();
+ let path = temp_config_path("reload-config-invalid-ui-section");
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
+ std::fs::write(
+ &path,
+ "[keys]\nnew_workspace = \"g\"\n[ui.toast]\ndelivery = \"desktop\"\n",
+ )
+ .unwrap();
+ std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path);
+
+ let mut app = test_app();
+ app.state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
+ let report = app.reload_config();
+
+ assert_eq!(report.status, crate::config::ConfigReloadStatus::Partial);
+ 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
+ .as_deref()
+ .is_some_and(|message| message.contains("invalid ui config")));
+
+ 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");