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