Skip to main content

fresh/app/
toggle_actions.rs

1//! Toggle actions and configuration operations for the Editor.
2//!
3//! This module contains toggle methods and configuration operations:
4//! - Toggle line numbers, debug highlights, menu bar
5//! - Toggle mouse capture, mouse hover, inlay hints
6//! - Reset buffer settings
7//! - Config dump, save, and reload
8
9use crate::types::LspServerConfig;
10use rust_i18n::t;
11
12use crate::config::{Config, FileExplorerSide};
13use crate::config_io::{ConfigLayer, ConfigResolver};
14use crate::input::keybindings::KeybindingResolver;
15
16use super::Editor;
17
18impl Editor {
19    /// Toggle line numbers in the gutter for the active split.
20    ///
21    /// Line number visibility is stored per-split in `BufferViewState` so that
22    /// different splits of the same buffer can independently show/hide line numbers
23    /// (e.g., source mode shows them, compose mode hides them).
24    pub fn toggle_line_numbers(&mut self) {
25        let active_split = self
26            .windows
27            .get(&self.active_window)
28            .and_then(|w| w.buffers.splits())
29            .map(|(mgr, _)| mgr)
30            .expect("active window must have a populated split layout")
31            .active_split();
32        if let Some(vs) = self
33            .windows
34            .get_mut(&self.active_window)
35            .and_then(|w| w.split_view_states_mut())
36            .expect("active window must have a populated split layout")
37            .get_mut(&active_split)
38        {
39            let currently_shown = vs.show_line_numbers;
40            vs.show_line_numbers = !currently_shown;
41            if currently_shown {
42                self.set_status_message(t!("toggle.line_numbers_hidden").to_string());
43            } else {
44                self.set_status_message(t!("toggle.line_numbers_shown").to_string());
45            }
46        }
47    }
48
49    /// Kick off the full-screen wave animation: a crest of wave glyphs
50    /// rises from the bottom edge and bounces every painted cell — text,
51    /// gutter, menu bar, status bar — up, down, and sideways before they
52    /// spring back into place. Purely cosmetic; the underlying UI keeps
53    /// re-painting underneath and is restored intact once the wave ends.
54    pub fn trigger_wave_animation(&mut self) {
55        let area = ratatui::layout::Rect {
56            x: 0,
57            y: 0,
58            width: self.terminal_width,
59            height: self.terminal_height,
60        };
61        if area.width == 0 || area.height == 0 {
62            return;
63        }
64        // Long safety duration: the wave runs until the user dismisses it
65        // (any key / mouse), so this is just an upper bound, not the show's
66        // real length.
67        self.active_window_mut().animations.start(
68            area,
69            crate::view::animation::AnimationKind::Wave {
70                duration: std::time::Duration::from_secs(600),
71            },
72        );
73        self.set_status_message(t!("wave.triggered").to_string());
74    }
75
76    /// Whether the interactive wave animation is currently running in any
77    /// window (it persists until the user presses a key or moves the mouse).
78    pub fn wave_animation_active(&self) -> bool {
79        self.windows
80            .values()
81            .any(|w| w.animations.has_dismissable())
82    }
83
84    /// Dismiss the wave animation everywhere it's running.
85    pub fn cancel_wave_animation(&mut self) {
86        for w in self.windows.values_mut() {
87            w.animations.cancel_dismissable();
88        }
89    }
90
91    /// The configured screensaver idle threshold, or `None` when the
92    /// screensaver is disabled (turned off, or a zero-minute timeout).
93    pub fn screensaver_idle_timeout(&self) -> Option<std::time::Duration> {
94        let editor = &self.config.editor;
95        if editor.screensaver_enabled && editor.screensaver_idle_minutes > 0 {
96            Some(std::time::Duration::from_secs(
97                editor.screensaver_idle_minutes as u64 * 60,
98            ))
99        } else {
100            None
101        }
102    }
103
104    /// Start the wave animation as a screensaver if the editor has been
105    /// idle (no key/mouse input) for at least the configured threshold.
106    /// No-op — returns `false` — when the screensaver is disabled, the
107    /// idle time is below the threshold, or a wave is already running.
108    /// Returns `true` iff it started the screensaver on this call. The
109    /// main loop calls this each time it polls and finds no input event;
110    /// any real input both dismisses the wave and resets the idle clock.
111    pub fn maybe_start_screensaver(&mut self, idle: std::time::Duration) -> bool {
112        let Some(timeout) = self.screensaver_idle_timeout() else {
113            return false;
114        };
115        if idle < timeout || self.wave_animation_active() {
116            return false;
117        }
118        self.trigger_wave_animation();
119        true
120    }
121
122    /// Toggle menu bar visibility.
123    ///
124    /// `editor.show_menu_bar` is a global preference, so the toggle updates the
125    /// runtime config and persists the change to the user config layer (same
126    /// pattern as the file-explorer toggles). See issue #1156.
127    pub fn toggle_menu_bar(&mut self) {
128        let new_value = !self.active_window_mut().menu_bar_visible;
129        self.config_mut().editor.show_menu_bar = new_value;
130        self.active_window_mut().menu_bar_visible = new_value;
131        // When explicitly toggling, clear auto-show state
132        self.active_window_mut().menu_bar_auto_shown = false;
133        // Close any open menu when hiding the menu bar
134        if !self.active_window_mut().menu_bar_visible {
135            self.menu_state.close_menu();
136        }
137        self.persist_config_change("/editor/show_menu_bar", serde_json::Value::Bool(new_value));
138        let status = if self.active_window_mut().menu_bar_visible {
139            t!("toggle.menu_bar_shown")
140        } else {
141            t!("toggle.menu_bar_hidden")
142        };
143        self.set_status_message(status.to_string());
144    }
145
146    // `toggle_tab_bar` / `toggle_status_bar` / `toggle_prompt_line` and
147    // their `*_visible` getters live on `impl Window` — call them via
148    // `self.active_window_mut().toggle_tab_bar()` etc. (or read
149    // `active_window().tab_bar_visible` for the flag directly).
150
151    /// Toggle the file explorer side between left and right.
152    ///
153    /// Updates the runtime config and the active window's runtime side
154    /// shadow, then persists the change to the user config layer (same
155    /// pattern as `toggle_menu_bar`).
156    pub fn toggle_file_explorer_side(&mut self) {
157        let new_side = match self.config.file_explorer.side {
158            FileExplorerSide::Left => FileExplorerSide::Right,
159            FileExplorerSide::Right => FileExplorerSide::Left,
160        };
161        self.config_mut().file_explorer.side = new_side;
162        self.active_window_mut().file_explorer_side = new_side;
163        self.persist_config_change(
164            "/file_explorer/side",
165            serde_json::json!(match new_side {
166                FileExplorerSide::Left => "left",
167                FileExplorerSide::Right => "right",
168            }),
169        );
170        let status = match new_side {
171            FileExplorerSide::Left => t!("toggle.file_explorer_side_left"),
172            FileExplorerSide::Right => t!("toggle.file_explorer_side_right"),
173        };
174        self.set_status_message(status.to_string());
175    }
176
177    /// Toggle vertical scrollbar visibility
178    pub fn toggle_vertical_scrollbar(&mut self) {
179        let new_value = !self.config.editor.show_vertical_scrollbar;
180        self.config_mut().editor.show_vertical_scrollbar = new_value;
181        let status = if self.config.editor.show_vertical_scrollbar {
182            t!("toggle.vertical_scrollbar_shown")
183        } else {
184            t!("toggle.vertical_scrollbar_hidden")
185        };
186        self.set_status_message(status.to_string());
187    }
188
189    /// Toggle horizontal scrollbar visibility
190    pub fn toggle_horizontal_scrollbar(&mut self) {
191        let new_value = !self.config.editor.show_horizontal_scrollbar;
192        self.config_mut().editor.show_horizontal_scrollbar = new_value;
193        let status = if self.config.editor.show_horizontal_scrollbar {
194            t!("toggle.horizontal_scrollbar_shown")
195        } else {
196            t!("toggle.horizontal_scrollbar_hidden")
197        };
198        self.set_status_message(status.to_string());
199    }
200
201    /// Reset buffer settings (tab_size, use_tabs, auto_close, whitespace visibility) to config defaults
202    pub fn reset_buffer_settings(&mut self) {
203        use crate::config::WhitespaceVisibility;
204        let buffer_id = self.active_buffer();
205
206        // Determine settings from config using buffer's stored language
207        let mut whitespace = WhitespaceVisibility::from_editor_config(&self.config.editor);
208        let mut auto_close = self.config.editor.auto_close;
209        let mut word_characters = String::new();
210        let (tab_size, use_tabs) = if let Some(state) = self
211            .windows
212            .get(&self.active_window)
213            .map(|w| &w.buffers)
214            .expect("active window present")
215            .get(&buffer_id)
216        {
217            let language = &state.language;
218            if let Some(lang_config) = self.config.languages.get(language) {
219                whitespace =
220                    whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
221                // Auto close: language override (only if globally enabled)
222                if auto_close {
223                    if let Some(lang_auto_close) = lang_config.auto_close {
224                        auto_close = lang_auto_close;
225                    }
226                }
227                if let Some(ref wc) = lang_config.word_characters {
228                    word_characters = wc.clone();
229                }
230                (
231                    lang_config.tab_size.unwrap_or(self.config.editor.tab_size),
232                    lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs),
233                )
234            } else {
235                (self.config.editor.tab_size, self.config.editor.use_tabs)
236            }
237        } else {
238            (self.config.editor.tab_size, self.config.editor.use_tabs)
239        };
240
241        // Apply settings to buffer
242        if let Some(state) = self
243            .windows
244            .get_mut(&self.active_window)
245            .map(|w| &mut w.buffers)
246            .expect("active window present")
247            .get_mut(&buffer_id)
248        {
249            state.buffer_settings.tab_size = tab_size;
250            state.buffer_settings.use_tabs = use_tabs;
251            state.buffer_settings.auto_close = auto_close;
252            state.buffer_settings.whitespace = whitespace;
253            state.buffer_settings.word_characters = word_characters;
254        }
255
256        self.set_status_message(t!("toggle.buffer_settings_reset").to_string());
257    }
258
259    /// Toggle mouse capture on/off
260    pub fn toggle_mouse_capture(&mut self) {
261        use std::io::stdout;
262
263        self.active_window_mut().mouse_enabled = !self.active_window_mut().mouse_enabled;
264
265        if self.active_window_mut().mouse_enabled {
266            // Best-effort terminal mouse capture toggle.
267            #[allow(clippy::let_underscore_must_use)]
268            let _ = crossterm::execute!(stdout(), crossterm::event::EnableMouseCapture);
269            self.set_status_message(t!("toggle.mouse_capture_enabled").to_string());
270        } else {
271            // Best-effort terminal mouse capture toggle.
272            #[allow(clippy::let_underscore_must_use)]
273            let _ = crossterm::execute!(stdout(), crossterm::event::DisableMouseCapture);
274            self.set_status_message(t!("toggle.mouse_capture_disabled").to_string());
275        }
276    }
277
278    /// Check if mouse capture is enabled
279    pub fn is_mouse_enabled(&self) -> bool {
280        self.active_window().mouse_enabled
281    }
282
283    /// Toggle mouse hover for LSP on/off
284    ///
285    /// On Windows, this also switches the mouse tracking mode: mode 1003
286    /// (all motion) when enabled, mode 1002 (cell motion) when disabled.
287    pub fn toggle_mouse_hover(&mut self) {
288        let new_value = !self.config.editor.mouse_hover_enabled;
289        self.config_mut().editor.mouse_hover_enabled = new_value;
290
291        if self.config.editor.mouse_hover_enabled {
292            self.set_status_message(t!("toggle.mouse_hover_enabled").to_string());
293        } else {
294            // Clear any pending hover state
295            self.active_window_mut().mouse_state.lsp_hover_state = None;
296            self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
297            self.set_status_message(t!("toggle.mouse_hover_disabled").to_string());
298        }
299
300        // On Windows, switch mouse tracking mode to match
301        #[cfg(windows)]
302        {
303            let mode = if self.config.editor.mouse_hover_enabled {
304                fresh_winterm::MouseMode::AllMotion
305            } else {
306                fresh_winterm::MouseMode::CellMotion
307            };
308            if let Err(e) = fresh_winterm::set_mouse_mode(mode) {
309                tracing::error!("Failed to switch mouse mode: {}", e);
310            }
311        }
312    }
313
314    /// Check if mouse hover is enabled
315    pub fn is_mouse_hover_enabled(&self) -> bool {
316        self.config.editor.mouse_hover_enabled
317    }
318
319    /// Set GPM active flag (enables software mouse cursor rendering)
320    ///
321    /// When GPM is used for mouse input on Linux consoles, we need to draw
322    /// our own mouse cursor because GPM can't draw on the alternate screen
323    /// buffer used by TUI applications.
324    pub fn set_gpm_active(&mut self, active: bool) {
325        self.active_window_mut().gpm_active = active;
326    }
327
328    /// Toggle inlay hints visibility
329    pub fn toggle_inlay_hints(&mut self) {
330        let new_value = !self.config.editor.enable_inlay_hints;
331        self.config_mut().editor.enable_inlay_hints = new_value;
332        // `Window::send_lsp_changes_for_buffer` reads
333        // `resources.config.editor.enable_inlay_hints`; sync so the per-edit
334        // LSP refresh sees the new value without waiting for a reload.
335        self.sync_windows_config();
336
337        if self.config.editor.enable_inlay_hints {
338            // Re-request inlay hints for the active buffer
339            self.request_inlay_hints_for_active_buffer();
340            self.set_status_message(t!("toggle.inlay_hints_enabled").to_string());
341        } else {
342            // Clear inlay hints from all buffers
343            for (_, state) in self
344                .windows
345                .get_mut(&self.active_window)
346                .map(|w| &mut w.buffers)
347                .expect("active window present")
348            {
349                state.virtual_texts.clear(&mut state.marker_list);
350            }
351            self.set_status_message(t!("toggle.inlay_hints_disabled").to_string());
352        }
353    }
354
355    /// Dump the current configuration to the user's config file
356    pub fn dump_config(&mut self) {
357        // Create the config directory if it doesn't exist
358        if let Err(e) = self
359            .authority()
360            .filesystem
361            .create_dir_all(&self.dir_context.config_dir)
362        {
363            self.set_status_message(
364                t!("error.config_dir_failed", error = e.to_string()).to_string(),
365            );
366            return;
367        }
368
369        let config_path = self.dir_context.config_path();
370        let resolver =
371            ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
372
373        // Save the config to user layer
374        match resolver.save_to_layer(&self.config, ConfigLayer::User) {
375            Ok(()) => {
376                // Open the saved config file in a new buffer
377                match self.open_file(&config_path) {
378                    Ok(_buffer_id) => {
379                        self.set_status_message(
380                            t!("config.saved", path = config_path.display().to_string())
381                                .to_string(),
382                        );
383                    }
384                    Err(e) => {
385                        // Check if this is a large file encoding confirmation error
386                        if let Some(confirmation) =
387                            e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
388                        {
389                            self.start_large_file_encoding_confirmation(confirmation);
390                        } else {
391                            self.set_status_message(
392                                t!("config.saved_failed_open", error = e.to_string()).to_string(),
393                            );
394                        }
395                    }
396                }
397            }
398            Err(e) => {
399                self.set_status_message(
400                    t!("error.config_save_failed", error = e.to_string()).to_string(),
401                );
402            }
403        }
404    }
405
406    /// Save the current configuration to file (without opening it)
407    ///
408    /// Returns Ok(()) on success, or an error message on failure
409    pub fn save_config(&self) -> Result<(), String> {
410        // Create the config directory if it doesn't exist
411        self.authority()
412            .filesystem
413            .create_dir_all(&self.dir_context.config_dir)
414            .map_err(|e| format!("Failed to create config directory: {}", e))?;
415
416        let resolver =
417            ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
418        resolver
419            .save_to_layer(&self.config, ConfigLayer::User)
420            .map_err(|e| format!("Failed to save config: {}", e))
421    }
422
423    /// Reload configuration from the config file
424    ///
425    /// This reloads the config from disk, applies runtime changes (theme, keybindings),
426    /// and emits a config_changed event so plugins can update their state accordingly.
427    /// Uses the layered config system to properly merge with defaults.
428    pub fn reload_config(&mut self) {
429        let old_theme = self.config.theme.clone();
430        self.set_config(Config::load_with_layers(
431            &self.dir_context,
432            self.working_dir(),
433        ));
434
435        // Refresh cached raw user config for plugins
436        self.set_user_config_raw(Config::read_user_config_raw(self.working_dir()));
437
438        // Apply theme change if needed
439        if old_theme != self.config.theme {
440            if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
441                *self.theme.write().unwrap() = theme;
442                self.start_theme_transition_animation();
443                tracing::info!("Theme changed to '{}'", self.config.theme.0);
444            } else {
445                tracing::error!("Theme '{}' not found", self.config.theme.0);
446            }
447        }
448
449        // Always reload keybindings (complex types don't implement PartialEq)
450        *self.keybindings.write().unwrap() = KeybindingResolver::new(&self.config);
451
452        // Update clipboard configuration
453        self.clipboard.apply_config(&self.config.clipboard);
454
455        // Apply bar visibility changes immediately
456        self.active_window_mut().menu_bar_visible = self.config.editor.show_menu_bar;
457        self.active_window_mut().tab_bar_visible = self.config.editor.show_tab_bar;
458        self.active_window_mut().status_bar_visible = self.config.editor.show_status_bar;
459        self.active_window_mut().prompt_line_visible = self.config.editor.show_prompt_line;
460
461        // Update LSP configs
462        let __active_id = self.active_window;
463        if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
464            lsp.set_globally_enabled(self.config.lsp_enabled);
465            for (language, lsp_configs) in &self.config.lsp {
466                lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
467            }
468            // Configure universal (global) LSP servers
469            let universal_servers: Vec<LspServerConfig> = self
470                .config
471                .universal_lsp
472                .values()
473                .flat_map(|lc| lc.as_slice().to_vec())
474                .filter(|c| c.enabled)
475                .collect();
476            lsp.set_universal_configs(universal_servers);
477        }
478
479        // Emit event so plugins know config changed
480        let config_path = Config::find_config_path(self.working_dir());
481        self.emit_event(
482            "config_changed",
483            serde_json::json!({
484                "path": config_path.map(|p| p.to_string_lossy().into_owned()),
485            }),
486        );
487    }
488
489    /// Reload the theme registry from disk.
490    ///
491    /// Call this after installing new theme packages or saving new themes.
492    /// This rescans all theme directories and updates the available themes list.
493    pub fn reload_themes(&mut self) {
494        use crate::view::theme::ThemeLoader;
495
496        let theme_loader = ThemeLoader::new(self.dir_context.themes_dir());
497        self.theme_registry = std::sync::Arc::new(theme_loader.load_all(&[]));
498        self.expanded_menus_cache.invalidate();
499
500        // Propagate the new registry to every window's resources so
501        // window-side reads see the updated catalogue. (Theme registry
502        // is `Arc<ThemeRegistry>` not `Arc<RwLock<>>`, so swapping the
503        // Editor's pointer leaves Window clones stale unless we sync.)
504        for w in self.windows.values_mut() {
505            w.resources.theme_registry = self.theme_registry.clone();
506        }
507
508        // Update shared theme cache for plugin access
509        *self.theme_cache.write().unwrap() = self.theme_registry.to_json_map();
510
511        // Re-apply current theme if it still exists, otherwise it might have been updated
512        if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
513            *self.theme.write().unwrap() = theme;
514        }
515
516        tracing::info!(
517            "Theme registry reloaded ({} themes)",
518            self.theme_registry.len()
519        );
520
521        // Emit event so plugins know themes changed
522        self.emit_event("themes_changed", serde_json::json!({}));
523    }
524
525    /// Persist a single config change to the user config file.
526    ///
527    /// Used when toggling settings via menu/command palette so that
528    /// the change is saved immediately (matching the settings UI behavior).
529    pub(super) fn persist_config_change(&self, json_pointer: &str, value: serde_json::Value) {
530        let resolver =
531            ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
532        let changes = std::collections::HashMap::from([(json_pointer.to_string(), value)]);
533        let deletions = std::collections::HashSet::new();
534        if let Err(e) = resolver.save_changes_to_layer(&changes, &deletions, ConfigLayer::User) {
535            tracing::error!("Failed to persist config change {}: {}", json_pointer, e);
536        }
537    }
538}