fresh/app/
toggle_actions.rs1use 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 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 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 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 pub fn wave_animation_active(&self) -> bool {
79 self.windows
80 .values()
81 .any(|w| w.animations.has_dismissable())
82 }
83
84 pub fn cancel_wave_animation(&mut self) {
86 for w in self.windows.values_mut() {
87 w.animations.cancel_dismissable();
88 }
89 }
90
91 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 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 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 self.active_window_mut().menu_bar_auto_shown = false;
133 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 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 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 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 pub fn reset_buffer_settings(&mut self) {
203 use crate::config::WhitespaceVisibility;
204 let buffer_id = self.active_buffer();
205
206 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 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 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 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 #[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 #[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 pub fn is_mouse_enabled(&self) -> bool {
280 self.active_window().mouse_enabled
281 }
282
283 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 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 #[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 pub fn is_mouse_hover_enabled(&self) -> bool {
316 self.config.editor.mouse_hover_enabled
317 }
318
319 pub fn set_gpm_active(&mut self, active: bool) {
325 self.active_window_mut().gpm_active = active;
326 }
327
328 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 self.sync_windows_config();
336
337 if self.config.editor.enable_inlay_hints {
338 self.request_inlay_hints_for_active_buffer();
340 self.set_status_message(t!("toggle.inlay_hints_enabled").to_string());
341 } else {
342 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 pub fn dump_config(&mut self) {
357 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 match resolver.save_to_layer(&self.config, ConfigLayer::User) {
375 Ok(()) => {
376 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 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 pub fn save_config(&self) -> Result<(), String> {
410 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 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 self.set_user_config_raw(Config::read_user_config_raw(self.working_dir()));
437
438 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 *self.keybindings.write().unwrap() = KeybindingResolver::new(&self.config);
451
452 self.clipboard.apply_config(&self.config.clipboard);
454
455 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 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 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 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 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 for w in self.windows.values_mut() {
505 w.resources.theme_registry = self.theme_registry.clone();
506 }
507
508 *self.theme_cache.write().unwrap() = self.theme_registry.to_json_map();
510
511 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 self.emit_event("themes_changed", serde_json::json!({}));
523 }
524
525 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}