Skip to main content

fresh/app/
settings_actions.rs

1//! Settings modal UI operations for the Editor.
2//!
3//! This module contains all methods related to the settings modal:
4//! - Opening/closing the settings modal
5//! - Saving settings to config
6//! - Navigation (up/down)
7//! - Activating/toggling settings
8//! - Incrementing/decrementing numeric values
9
10use crate::config_io::{ConfigLayer, ConfigResolver};
11use crate::input::keybindings::KeybindingResolver;
12use anyhow::Result as AnyhowResult;
13use rust_i18n::t;
14
15use super::Editor;
16
17impl Editor {
18    /// Open the settings modal
19    pub fn open_settings(&mut self) {
20        // Include schema at compile time
21        const SCHEMA_JSON: &str = include_str!("../../plugins/config-schema.json");
22
23        // Create settings state if not exists, or show existing
24        if self.settings_state.is_none() {
25            match crate::view::settings::SettingsState::new(SCHEMA_JSON, &self.config) {
26                Ok(mut state) => {
27                    // Load layer sources to show where each setting value comes from
28                    let resolver =
29                        ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
30                    if let Ok(sources) = resolver.get_layer_sources() {
31                        state.set_layer_sources(sources);
32                    }
33                    state.show();
34                    self.settings_state = Some(state);
35                }
36                Err(e) => {
37                    self.set_status_message(
38                        t!("settings.failed_to_open", error = e.to_string()).to_string(),
39                    );
40                }
41            }
42        } else if let Some(ref mut state) = self.settings_state {
43            state.show();
44        }
45    }
46
47    /// Close the settings modal
48    ///
49    /// If `save` is true and there are changes, they will be applied first.
50    pub fn close_settings(&mut self, save: bool) {
51        if save {
52            self.save_settings();
53        }
54        if let Some(ref mut state) = self.settings_state {
55            if !save && state.has_changes() {
56                // Discard changes
57                state.discard_changes();
58            }
59            state.hide();
60        }
61    }
62
63    /// Save the settings from the modal to config
64    pub fn save_settings(&mut self) {
65        let old_theme = self.config.theme.clone();
66        let old_locale = self.config.locale.clone();
67        let old_plugins = self.config.plugins.clone();
68
69        // Get target layer, new config, and the actual changes made
70        let (target_layer, new_config, pending_changes, pending_deletions) = {
71            if let Some(ref state) = self.settings_state {
72                if !state.has_changes() {
73                    return;
74                }
75                match state.apply_changes(&self.config) {
76                    Ok(config) => (
77                        state.target_layer,
78                        config,
79                        state.pending_changes.clone(),
80                        state.pending_deletions.clone(),
81                    ),
82                    Err(e) => {
83                        self.set_status_message(
84                            t!("settings.failed_to_apply", error = e.to_string()).to_string(),
85                        );
86                        return;
87                    }
88                }
89            } else {
90                return;
91            }
92        };
93
94        // Apply the new config
95        self.config = new_config.clone();
96
97        // Apply runtime changes
98        if old_theme != self.config.theme {
99            if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
100                self.theme = theme;
101                tracing::info!("Theme changed to '{}'", self.config.theme.0);
102            } else {
103                tracing::error!("Theme '{}' not found", self.config.theme.0);
104                self.set_status_message(format!("Theme '{}' not found", self.config.theme.0));
105            }
106        }
107
108        // Apply locale change at runtime
109        if old_locale != self.config.locale {
110            if let Some(locale) = self.config.locale.as_option() {
111                crate::i18n::set_locale(locale);
112                // Regenerate menus with the new locale
113                self.menus = crate::config::MenuConfig::translated();
114                tracing::info!("Locale changed to '{}'", locale);
115            } else {
116                // Auto-detect from environment
117                crate::i18n::init();
118                self.menus = crate::config::MenuConfig::translated();
119                tracing::info!("Locale reset to auto-detect");
120            }
121            // Refresh command palette commands with new locale
122            if let Ok(mut registry) = self.command_registry.write() {
123                registry.refresh_builtin_commands();
124            }
125        }
126
127        // Handle plugin enable/disable changes
128        self.apply_plugin_config_changes(&old_plugins);
129
130        // Update keybindings
131        self.keybindings = KeybindingResolver::new(&self.config);
132
133        // Save ONLY the changes to disk (preserves external edits to the config file)
134        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
135
136        let layer_name = match target_layer {
137            ConfigLayer::User => "User",
138            ConfigLayer::Project => "Project",
139            ConfigLayer::Session => "Session",
140            ConfigLayer::System => "System", // Should never happen
141        };
142
143        match resolver.save_changes_to_layer(&pending_changes, &pending_deletions, target_layer) {
144            Ok(()) => {
145                self.set_status_message(
146                    t!("settings.saved_to_layer", layer = layer_name).to_string(),
147                );
148                // Clear settings state entirely so next open creates fresh state
149                // from the updated config. This fixes issue #474 where reopening
150                // settings after save would show stale values.
151                self.settings_state = None;
152            }
153            Err(e) => {
154                self.set_status_message(
155                    t!("settings.failed_to_save", error = e.to_string()).to_string(),
156                );
157            }
158        }
159    }
160
161    /// Open the config file for the specified layer in the editor.
162    /// Creates the file with default template if it doesn't exist.
163    /// If there are pending changes in the Settings UI, warns the user and doesn't proceed.
164    pub fn open_config_file(&mut self, layer: ConfigLayer) -> AnyhowResult<()> {
165        // Check for pending changes before opening config file
166        if let Some(ref state) = self.settings_state {
167            if state.has_changes() {
168                self.set_status_message(t!("settings.pending_changes").to_string());
169                return Ok(());
170            }
171        }
172
173        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
174
175        let path = match layer {
176            ConfigLayer::User => resolver.user_config_path(),
177            ConfigLayer::Project => resolver.project_config_write_path(),
178            ConfigLayer::Session => resolver.session_config_path(),
179            ConfigLayer::System => {
180                self.set_status_message(t!("settings.cannot_edit_system").to_string());
181                return Ok(());
182            }
183        };
184
185        // Create parent directory if needed
186        if let Some(parent) = path.parent() {
187            self.filesystem.create_dir_all(parent)?;
188        }
189
190        // Create file with template if it doesn't exist
191        if !self.filesystem.exists(&path) {
192            let template = match layer {
193                ConfigLayer::User => {
194                    r#"{
195  "version": 1,
196  "theme": "default",
197  "editor": {
198    "tab_size": 4,
199    "line_numbers": true
200  }
201}
202"#
203                }
204                ConfigLayer::Project => {
205                    r#"{
206  "version": 1,
207  "editor": {
208    "tab_size": 4
209  },
210  "languages": {}
211}
212"#
213                }
214                ConfigLayer::Session => {
215                    r#"{
216  "version": 1
217}
218"#
219                }
220                ConfigLayer::System => unreachable!(),
221            };
222            self.filesystem.write_file(&path, template.as_bytes())?;
223        }
224
225        // Close settings and open the config file
226        self.settings_state = None;
227        self.open_file(&path)?;
228
229        let layer_name = match layer {
230            ConfigLayer::User => "User",
231            ConfigLayer::Project => "Project",
232            ConfigLayer::Session => "Session",
233            ConfigLayer::System => "System",
234        };
235        self.set_status_message(
236            t!(
237                "settings.editing_config",
238                layer = layer_name,
239                path = path.display().to_string()
240            )
241            .to_string(),
242        );
243
244        Ok(())
245    }
246
247    /// Navigate settings up
248    pub fn settings_navigate_up(&mut self) {
249        if let Some(ref mut state) = self.settings_state {
250            state.select_prev();
251        }
252    }
253
254    /// Navigate settings down
255    pub fn settings_navigate_down(&mut self) {
256        if let Some(ref mut state) = self.settings_state {
257            state.select_next();
258        }
259    }
260
261    /// Activate/toggle the currently selected setting
262    pub fn settings_activate_current(&mut self) {
263        use crate::view::settings::items::SettingControl;
264        use crate::view::settings::FocusPanel;
265
266        // Check if we're in the Footer panel - handle button activation
267        let focus_panel = self
268            .settings_state
269            .as_ref()
270            .map(|s| s.focus_panel())
271            .unwrap_or(FocusPanel::Categories);
272
273        if focus_panel == FocusPanel::Footer {
274            let button_index = self
275                .settings_state
276                .as_ref()
277                .map(|s| s.footer_button_index)
278                .unwrap_or(2);
279            match button_index {
280                0 => {
281                    // Layer button - cycle target layer
282                    if let Some(ref mut state) = self.settings_state {
283                        state.cycle_target_layer();
284                    }
285                }
286                1 => {
287                    // Reset button
288                    if let Some(ref mut state) = self.settings_state {
289                        state.reset_current_to_default();
290                    }
291                }
292                2 => {
293                    // Save button - save and close
294                    self.close_settings(true);
295                }
296                3 => {
297                    // Cancel button
298                    self.close_settings(false);
299                }
300                _ => {}
301            }
302            return;
303        }
304
305        // When Categories panel is focused, Enter does nothing to settings controls
306        // (keys should not leak to the right panel)
307        if focus_panel == FocusPanel::Categories {
308            return;
309        }
310
311        // Get the current item's control type to determine action
312        let control_type = {
313            if let Some(ref state) = self.settings_state {
314                state.current_item().map(|item| match &item.control {
315                    SettingControl::Toggle(_) => "toggle",
316                    SettingControl::Number(_) => "number",
317                    SettingControl::Dropdown(_) => "dropdown",
318                    SettingControl::Text(_) => "text",
319                    SettingControl::TextList(_) => "textlist",
320                    SettingControl::Map(_) => "map",
321                    SettingControl::ObjectArray(_) => "objectarray",
322                    SettingControl::Json(_) => "json",
323                    SettingControl::Complex { .. } => "complex",
324                })
325            } else {
326                None
327            }
328        };
329
330        // Perform the action based on control type
331        match control_type {
332            Some("toggle") => {
333                if let Some(ref mut state) = self.settings_state {
334                    if let Some(item) = state.current_item_mut() {
335                        if let SettingControl::Toggle(ref mut toggle_state) = item.control {
336                            toggle_state.checked = !toggle_state.checked;
337                        }
338                    }
339                    state.on_value_changed();
340                }
341            }
342            Some("dropdown") => {
343                // Toggle dropdown open/closed, or confirm selection if open
344                if let Some(ref mut state) = self.settings_state {
345                    if state.is_dropdown_open() {
346                        state.dropdown_confirm();
347                    } else {
348                        state.dropdown_toggle();
349                    }
350                }
351            }
352            Some("textlist") => {
353                // Enter text editing mode for TextList controls
354                if let Some(ref mut state) = self.settings_state {
355                    state.start_editing();
356                }
357            }
358            Some("map") => {
359                // For Map controls: check if map has a value schema (supports entry dialogs)
360                if let Some(ref mut state) = self.settings_state {
361                    if let Some(item) = state.current_item_mut() {
362                        if let SettingControl::Map(ref mut map_state) = item.control {
363                            if map_state.focused_entry.is_none() {
364                                // On add-new row: open dialog with empty key
365                                if map_state.value_schema.is_some() {
366                                    state.open_add_entry_dialog();
367                                }
368                            } else if map_state.value_schema.is_some() {
369                                // Map has schema: open entry dialog
370                                state.open_entry_dialog();
371                            } else {
372                                // For other maps: toggle expanded
373                                if let Some(idx) = map_state.focused_entry {
374                                    if map_state.expanded.contains(&idx) {
375                                        map_state.expanded.retain(|&i| i != idx);
376                                    } else {
377                                        map_state.expanded.push(idx);
378                                    }
379                                }
380                            }
381                        }
382                    }
383                    state.on_value_changed();
384                }
385            }
386            Some("text") => {
387                // For Text controls: enter text editing mode
388                if let Some(ref mut state) = self.settings_state {
389                    state.start_editing();
390                }
391            }
392            Some("number") => {
393                // For Number controls: enter number editing mode
394                if let Some(ref mut state) = self.settings_state {
395                    state.start_number_editing();
396                }
397            }
398            _ => {}
399        }
400    }
401
402    /// Increment the current setting value (for Number and Dropdown controls)
403    pub fn settings_increment_current(&mut self) {
404        use crate::view::settings::items::SettingControl;
405        use crate::view::settings::FocusPanel;
406
407        // Check if we're in the Footer panel - navigate buttons instead
408        let focus_panel = self
409            .settings_state
410            .as_ref()
411            .map(|s| s.focus_panel())
412            .unwrap_or(FocusPanel::Categories);
413
414        if focus_panel == FocusPanel::Footer {
415            if let Some(ref mut state) = self.settings_state {
416                // Navigate to next footer button (wrapping around)
417                state.footer_button_index = (state.footer_button_index + 1) % 4;
418            }
419            return;
420        }
421
422        // When Categories panel is focused, Left/Right don't affect settings controls
423        if focus_panel == FocusPanel::Categories {
424            return;
425        }
426
427        let control_type = {
428            if let Some(ref state) = self.settings_state {
429                state.current_item().map(|item| match &item.control {
430                    SettingControl::Number(_) => "number",
431                    SettingControl::Dropdown(_) => "dropdown",
432                    _ => "other",
433                })
434            } else {
435                None
436            }
437        };
438
439        match control_type {
440            Some("number") => {
441                if let Some(ref mut state) = self.settings_state {
442                    if let Some(item) = state.current_item_mut() {
443                        if let SettingControl::Number(ref mut num_state) = item.control {
444                            num_state.increment();
445                        }
446                    }
447                    state.on_value_changed();
448                }
449            }
450            Some("dropdown") => {
451                if let Some(ref mut state) = self.settings_state {
452                    if let Some(item) = state.current_item_mut() {
453                        if let SettingControl::Dropdown(ref mut dropdown_state) = item.control {
454                            dropdown_state.select_next();
455                        }
456                    }
457                    state.on_value_changed();
458                }
459            }
460            _ => {}
461        }
462    }
463
464    /// Decrement the current setting value (for Number and Dropdown controls)
465    pub fn settings_decrement_current(&mut self) {
466        use crate::view::settings::items::SettingControl;
467        use crate::view::settings::FocusPanel;
468
469        // Check if we're in the Footer panel - navigate buttons instead
470        let focus_panel = self
471            .settings_state
472            .as_ref()
473            .map(|s| s.focus_panel())
474            .unwrap_or(FocusPanel::Categories);
475
476        if focus_panel == FocusPanel::Footer {
477            if let Some(ref mut state) = self.settings_state {
478                // Navigate to previous footer button (wrapping around)
479                state.footer_button_index = if state.footer_button_index == 0 {
480                    3
481                } else {
482                    state.footer_button_index - 1
483                };
484            }
485            return;
486        }
487
488        // When Categories panel is focused, Left/Right don't affect settings controls
489        if focus_panel == FocusPanel::Categories {
490            return;
491        }
492
493        let control_type = {
494            if let Some(ref state) = self.settings_state {
495                state.current_item().map(|item| match &item.control {
496                    SettingControl::Number(_) => "number",
497                    SettingControl::Dropdown(_) => "dropdown",
498                    _ => "other",
499                })
500            } else {
501                None
502            }
503        };
504
505        match control_type {
506            Some("number") => {
507                if let Some(ref mut state) = self.settings_state {
508                    if let Some(item) = state.current_item_mut() {
509                        if let SettingControl::Number(ref mut num_state) = item.control {
510                            num_state.decrement();
511                        }
512                    }
513                    state.on_value_changed();
514                }
515            }
516            Some("dropdown") => {
517                if let Some(ref mut state) = self.settings_state {
518                    if let Some(item) = state.current_item_mut() {
519                        if let SettingControl::Dropdown(ref mut dropdown_state) = item.control {
520                            dropdown_state.select_prev();
521                        }
522                    }
523                    state.on_value_changed();
524                }
525            }
526            _ => {}
527        }
528    }
529
530    /// Apply plugin configuration changes by loading/unloading plugins as needed
531    fn apply_plugin_config_changes(
532        &mut self,
533        old_plugins: &std::collections::HashMap<String, crate::config::PluginConfig>,
534    ) {
535        // Collect changes first to avoid borrow issues
536        let changes: Vec<_> = self
537            .config
538            .plugins
539            .iter()
540            .filter_map(|(name, new_config)| {
541                let was_enabled = old_plugins.get(name).map(|c| c.enabled).unwrap_or(true);
542                if new_config.enabled != was_enabled {
543                    Some((name.clone(), new_config.enabled, new_config.path.clone()))
544                } else {
545                    None
546                }
547            })
548            .collect();
549
550        // Apply changes
551        for (name, now_enabled, path) in changes {
552            if now_enabled {
553                // Plugin was disabled, now enabled - load it
554                if let Some(ref path) = path {
555                    tracing::info!("Loading newly enabled plugin: {}", name);
556                    if let Err(e) = self.plugin_manager.load_plugin(path) {
557                        tracing::error!("Failed to load plugin '{}': {}", name, e);
558                        self.set_status_message(format!("Failed to load plugin '{}': {}", name, e));
559                    }
560                }
561            } else {
562                // Plugin was enabled, now disabled - unload it
563                tracing::info!("Unloading disabled plugin: {}", name);
564                if let Err(e) = self.plugin_manager.unload_plugin(&name) {
565                    tracing::error!("Failed to unload plugin '{}': {}", name, e);
566                    self.set_status_message(format!("Failed to unload plugin '{}': {}", name, e));
567                }
568            }
569        }
570    }
571}