fresh/app/
toggle_actions.rs1use crate::types::LspServerConfig;
10use rust_i18n::t;
11
12use crate::config::{Config, FileExplorerSide};
13use crate::config_io::{ConfigLayer, ConfigResolver};
14
15use super::Editor;
16
17impl Editor {
18 pub fn toggle_line_numbers(&mut self) {
29 let active_split = self
30 .windows
31 .get(&self.active_window)
32 .and_then(|w| w.buffers.splits())
33 .map(|(mgr, _)| mgr)
34 .expect("active window must have a populated split layout")
35 .active_split();
36 let Some(new_value) = self
37 .windows
38 .get_mut(&self.active_window)
39 .and_then(|w| w.split_view_states_mut())
40 .expect("active window must have a populated split layout")
41 .get_mut(&active_split)
42 .map(|vs| {
43 let new_value = !vs.show_line_numbers;
44 vs.show_line_numbers = new_value;
45 vs.line_numbers_override = None;
49 new_value
50 })
51 else {
52 return;
53 };
54
55 self.config_mut().editor.line_numbers = new_value;
59 self.persist_config_change("/editor/line_numbers", serde_json::Value::Bool(new_value));
60
61 let status = if new_value {
62 t!("toggle.line_numbers_shown")
63 } else {
64 t!("toggle.line_numbers_hidden")
65 };
66 self.set_status_message(status.to_string());
67 }
68
69 pub fn toggle_line_numbers_current_buffer(&mut self) {
77 let active_split = self
78 .windows
79 .get(&self.active_window)
80 .and_then(|w| w.buffers.splits())
81 .map(|(mgr, _)| mgr)
82 .expect("active window must have a populated split layout")
83 .active_split();
84 let Some(new_value) = self
85 .windows
86 .get_mut(&self.active_window)
87 .and_then(|w| w.split_view_states_mut())
88 .expect("active window must have a populated split layout")
89 .get_mut(&active_split)
90 .map(|vs| {
91 let new_value = !vs.show_line_numbers;
94 vs.show_line_numbers = new_value;
95 vs.line_numbers_override = Some(new_value);
96 new_value
97 })
98 else {
99 return;
100 };
101
102 let status = if new_value {
103 t!("toggle.line_numbers_shown")
104 } else {
105 t!("toggle.line_numbers_hidden")
106 };
107 self.set_status_message(status.to_string());
108 }
109
110 pub fn toggle_line_wrap_current_buffer(&mut self) {
117 let active_split = self
118 .windows
119 .get(&self.active_window)
120 .and_then(|w| w.buffers.splits())
121 .map(|(mgr, _)| mgr)
122 .expect("active window must have a populated split layout")
123 .active_split();
124 let buffer_id = self.active_buffer();
125 let wrap_column = self
126 .active_window()
127 .resolve_wrap_column_for_buffer(buffer_id);
128 let wrap_indent = self.config.editor.wrap_indent;
129 let Some(new_value) = self
130 .windows
131 .get_mut(&self.active_window)
132 .and_then(|w| w.split_view_states_mut())
133 .expect("active window must have a populated split layout")
134 .get_mut(&active_split)
135 .map(|vs| {
136 let new_value = !vs.viewport.line_wrap_enabled;
137 vs.viewport.line_wrap_enabled = new_value;
138 vs.viewport.wrap_indent = wrap_indent;
139 vs.viewport.wrap_column = wrap_column;
140 vs.line_wrap_override = Some(new_value);
141 new_value
142 })
143 else {
144 return;
145 };
146
147 let state = if new_value {
148 t!("view.state_enabled").to_string()
149 } else {
150 t!("view.state_disabled").to_string()
151 };
152 self.set_status_message(t!("view.line_wrap_state", state = state).to_string());
153 }
154
155 pub fn trigger_wave_animation(&mut self) {
161 let area = ratatui::layout::Rect {
162 x: 0,
163 y: 0,
164 width: self.terminal_width,
165 height: self.terminal_height,
166 };
167 if area.width == 0 || area.height == 0 {
168 return;
169 }
170 self.active_window_mut().animations.start(
174 area,
175 crate::view::animation::AnimationKind::Wave {
176 duration: std::time::Duration::from_secs(600),
177 },
178 );
179 self.set_status_message(t!("wave.triggered").to_string());
180 }
181
182 pub fn wave_animation_active(&self) -> bool {
185 self.windows
186 .values()
187 .any(|w| w.animations.has_dismissable())
188 }
189
190 pub fn cancel_wave_animation(&mut self) {
192 for w in self.windows.values_mut() {
193 w.animations.cancel_dismissable();
194 }
195 }
196
197 pub fn screensaver_idle_timeout(&self) -> Option<std::time::Duration> {
200 let editor = &self.config.editor;
201 if editor.screensaver_enabled && editor.screensaver_idle_minutes > 0 {
202 Some(std::time::Duration::from_secs(
203 editor.screensaver_idle_minutes as u64 * 60,
204 ))
205 } else {
206 None
207 }
208 }
209
210 pub fn maybe_start_screensaver(&mut self, idle: std::time::Duration) -> bool {
218 let Some(timeout) = self.screensaver_idle_timeout() else {
219 return false;
220 };
221 if idle < timeout || self.wave_animation_active() {
222 return false;
223 }
224 self.trigger_wave_animation();
225 true
226 }
227
228 pub fn toggle_menu_bar(&mut self) {
234 let new_value = !self.active_window_mut().menu_bar_visible;
235 self.config_mut().editor.show_menu_bar = new_value;
236 self.active_window_mut().menu_bar_visible = new_value;
237 self.active_window_mut().menu_bar_auto_shown = false;
239 if !self.active_window_mut().menu_bar_visible {
241 self.menu_state.close_menu();
242 }
243 self.persist_config_change("/editor/show_menu_bar", serde_json::Value::Bool(new_value));
244 let status = if self.active_window_mut().menu_bar_visible {
245 t!("toggle.menu_bar_shown")
246 } else {
247 t!("toggle.menu_bar_hidden")
248 };
249 self.set_status_message(status.to_string());
250 }
251
252 pub fn toggle_file_explorer_side(&mut self) {
263 let new_side = match self.config.file_explorer.side {
264 FileExplorerSide::Left => FileExplorerSide::Right,
265 FileExplorerSide::Right => FileExplorerSide::Left,
266 };
267 self.config_mut().file_explorer.side = new_side;
268 self.active_window_mut().file_explorer_side = new_side;
269 self.persist_config_change(
270 "/file_explorer/side",
271 serde_json::json!(match new_side {
272 FileExplorerSide::Left => "left",
273 FileExplorerSide::Right => "right",
274 }),
275 );
276 let status = match new_side {
277 FileExplorerSide::Left => t!("toggle.file_explorer_side_left"),
278 FileExplorerSide::Right => t!("toggle.file_explorer_side_right"),
279 };
280 self.set_status_message(status.to_string());
281 }
282
283 pub fn toggle_vertical_scrollbar(&mut self) {
285 let new_value = !self.config.editor.show_vertical_scrollbar;
286 self.config_mut().editor.show_vertical_scrollbar = new_value;
287 self.persist_config_change(
290 "/editor/show_vertical_scrollbar",
291 serde_json::Value::Bool(new_value),
292 );
293 let status = if new_value {
294 t!("toggle.vertical_scrollbar_shown")
295 } else {
296 t!("toggle.vertical_scrollbar_hidden")
297 };
298 self.set_status_message(status.to_string());
299 }
300
301 pub fn toggle_horizontal_scrollbar(&mut self) {
303 let new_value = !self.config.editor.show_horizontal_scrollbar;
304 self.config_mut().editor.show_horizontal_scrollbar = new_value;
305 self.persist_config_change(
308 "/editor/show_horizontal_scrollbar",
309 serde_json::Value::Bool(new_value),
310 );
311 let status = if new_value {
312 t!("toggle.horizontal_scrollbar_shown")
313 } else {
314 t!("toggle.horizontal_scrollbar_hidden")
315 };
316 self.set_status_message(status.to_string());
317 }
318
319 pub fn reset_buffer_settings(&mut self) {
321 use crate::config::WhitespaceVisibility;
322 let buffer_id = self.active_buffer();
323
324 let mut whitespace = WhitespaceVisibility::from_editor_config(&self.config.editor);
326 let mut auto_close = self.config.editor.auto_close;
327 let mut word_characters = String::new();
328 let (tab_size, use_tabs) = if let Some(state) = self
329 .windows
330 .get(&self.active_window)
331 .map(|w| &w.buffers)
332 .expect("active window present")
333 .get(&buffer_id)
334 {
335 let language = &state.language;
336 if let Some(lang_config) = self.config.languages.get(language) {
337 whitespace =
338 whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
339 if auto_close {
341 if let Some(lang_auto_close) = lang_config.auto_close {
342 auto_close = lang_auto_close;
343 }
344 }
345 if let Some(ref wc) = lang_config.word_characters {
346 word_characters = wc.clone();
347 }
348 (
349 lang_config.tab_size.unwrap_or(self.config.editor.tab_size),
350 lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs),
351 )
352 } else {
353 (self.config.editor.tab_size, self.config.editor.use_tabs)
354 }
355 } else {
356 (self.config.editor.tab_size, self.config.editor.use_tabs)
357 };
358
359 if let Some(state) = self
361 .windows
362 .get_mut(&self.active_window)
363 .map(|w| &mut w.buffers)
364 .expect("active window present")
365 .get_mut(&buffer_id)
366 {
367 state.buffer_settings.tab_size = tab_size;
368 state.buffer_settings.use_tabs = use_tabs;
369 state.buffer_settings.auto_close = auto_close;
370 state.buffer_settings.whitespace = whitespace;
371 state.buffer_settings.word_characters = word_characters;
372 }
373
374 self.set_status_message(t!("toggle.buffer_settings_reset").to_string());
375 }
376
377 pub fn toggle_mouse_capture(&mut self) {
379 use std::io::stdout;
380
381 self.active_window_mut().mouse_enabled = !self.active_window_mut().mouse_enabled;
382
383 if self.active_window_mut().mouse_enabled {
384 #[allow(clippy::let_underscore_must_use)]
386 let _ = crossterm::execute!(stdout(), crossterm::event::EnableMouseCapture);
387 self.set_status_message(t!("toggle.mouse_capture_enabled").to_string());
388 } else {
389 #[allow(clippy::let_underscore_must_use)]
391 let _ = crossterm::execute!(stdout(), crossterm::event::DisableMouseCapture);
392 self.set_status_message(t!("toggle.mouse_capture_disabled").to_string());
393 }
394 }
395
396 pub fn is_mouse_enabled(&self) -> bool {
398 self.active_window().mouse_enabled
399 }
400
401 pub fn toggle_mouse_hover(&mut self) {
406 let new_value = !self.config.editor.mouse_hover_enabled;
407 self.config_mut().editor.mouse_hover_enabled = new_value;
408
409 if self.config.editor.mouse_hover_enabled {
410 self.set_status_message(t!("toggle.mouse_hover_enabled").to_string());
411 } else {
412 self.active_window_mut().mouse_state.lsp_hover_state = None;
414 self.active_window_mut().mouse_state.lsp_hover_request_sent = false;
415 self.set_status_message(t!("toggle.mouse_hover_disabled").to_string());
416 }
417
418 #[cfg(windows)]
420 {
421 let mode = if self.config.editor.mouse_hover_enabled {
422 fresh_winterm::MouseMode::AllMotion
423 } else {
424 fresh_winterm::MouseMode::CellMotion
425 };
426 if let Err(e) = fresh_winterm::set_mouse_mode(mode) {
427 tracing::error!("Failed to switch mouse mode: {}", e);
428 }
429 }
430 }
431
432 pub fn is_mouse_hover_enabled(&self) -> bool {
434 self.config.editor.mouse_hover_enabled
435 }
436
437 pub fn set_gpm_active(&mut self, active: bool) {
443 self.active_window_mut().gpm_active = active;
444 }
445
446 pub fn toggle_inlay_hints(&mut self) {
448 let new_value = !self.config.editor.enable_inlay_hints;
449 self.config_mut().editor.enable_inlay_hints = new_value;
450 self.sync_windows_config();
454
455 if self.config.editor.enable_inlay_hints {
456 self.request_inlay_hints_for_active_buffer();
458 self.set_status_message(t!("toggle.inlay_hints_enabled").to_string());
459 } else {
460 for (_, state) in self
462 .windows
463 .get_mut(&self.active_window)
464 .map(|w| &mut w.buffers)
465 .expect("active window present")
466 {
467 state.virtual_texts.clear(&mut state.marker_list);
468 }
469 self.set_status_message(t!("toggle.inlay_hints_disabled").to_string());
470 }
471 }
472
473 pub fn dump_config(&mut self) {
475 if let Err(e) = self
477 .authority()
478 .filesystem
479 .create_dir_all(&self.dir_context.config_dir)
480 {
481 self.set_status_message(
482 t!("error.config_dir_failed", error = e.to_string()).to_string(),
483 );
484 return;
485 }
486
487 let config_path = self.dir_context.config_path();
488 let resolver =
489 ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
490
491 match resolver.save_to_layer(&self.config, ConfigLayer::User) {
493 Ok(()) => {
494 match self.open_file(&config_path) {
496 Ok(_buffer_id) => {
497 self.set_status_message(
498 t!("config.saved", path = config_path.display().to_string())
499 .to_string(),
500 );
501 }
502 Err(e) => {
503 if let Some(confirmation) =
505 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
506 {
507 self.start_large_file_encoding_confirmation(confirmation);
508 } else {
509 self.set_status_message(
510 t!("config.saved_failed_open", error = e.to_string()).to_string(),
511 );
512 }
513 }
514 }
515 }
516 Err(e) => {
517 self.set_status_message(
518 t!("error.config_save_failed", error = e.to_string()).to_string(),
519 );
520 }
521 }
522 }
523
524 pub fn save_config(&self) -> Result<(), String> {
528 self.authority()
530 .filesystem
531 .create_dir_all(&self.dir_context.config_dir)
532 .map_err(|e| format!("Failed to create config directory: {}", e))?;
533
534 let resolver =
535 ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
536 resolver
537 .save_to_layer(&self.config, ConfigLayer::User)
538 .map_err(|e| format!("Failed to save config: {}", e))
539 }
540
541 pub fn reload_config(&mut self) {
547 let old_theme = self.config.theme.clone();
548 self.set_config(Config::load_with_layers(
549 &self.dir_context,
550 self.working_dir(),
551 ));
552
553 self.set_user_config_raw(Config::read_user_config_raw(self.working_dir()));
555
556 if old_theme != self.config.theme {
558 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
559 *self.theme.write().unwrap() = theme;
560 self.start_theme_transition_animation();
561 tracing::info!("Theme changed to '{}'", self.config.theme.0);
562 } else {
563 tracing::error!("Theme '{}' not found", self.config.theme.0);
564 }
565 }
566
567 self.keybindings
570 .write()
571 .unwrap()
572 .reload_from_config(&self.config);
573
574 self.clipboard.apply_config(&self.config.clipboard);
576
577 self.active_window_mut().menu_bar_visible = self.config.editor.show_menu_bar;
579 self.active_window_mut().tab_bar_visible = self.config.editor.show_tab_bar;
580 self.active_window_mut().status_bar_visible = self.config.editor.show_status_bar;
581 self.active_window_mut().prompt_line_visible = self.config.editor.show_prompt_line;
582
583 let __active_id = self.active_window;
585 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
586 lsp.set_globally_enabled(self.config.lsp_enabled);
587 for (language, lsp_configs) in &self.config.lsp {
588 lsp.set_language_configs(language.clone(), lsp_configs.as_slice().to_vec());
589 }
590 let universal_servers: Vec<LspServerConfig> = self
592 .config
593 .universal_lsp
594 .values()
595 .flat_map(|lc| lc.as_slice().to_vec())
596 .filter(|c| c.enabled)
597 .collect();
598 lsp.set_universal_configs(universal_servers);
599 }
600
601 let config_path = Config::find_config_path(self.working_dir());
603 self.emit_event(
604 "config_changed",
605 serde_json::json!({
606 "path": config_path.map(|p| p.to_string_lossy().into_owned()),
607 }),
608 );
609 }
610
611 pub fn reload_themes(&mut self) {
616 use crate::view::theme::ThemeLoader;
617
618 let theme_loader = ThemeLoader::new(self.dir_context.themes_dir());
619 self.theme_registry = std::sync::Arc::new(theme_loader.load_all(&[]));
620 self.expanded_menus_cache.invalidate();
621
622 for w in self.windows.values_mut() {
627 w.resources.theme_registry = self.theme_registry.clone();
628 }
629
630 *self.theme_cache.write().unwrap() = self.theme_registry.to_json_map();
632
633 if let Some(theme) = self.theme_registry.get_cloned(&self.config.theme) {
635 *self.theme.write().unwrap() = theme;
636 }
637
638 tracing::info!(
639 "Theme registry reloaded ({} themes)",
640 self.theme_registry.len()
641 );
642
643 self.emit_event("themes_changed", serde_json::json!({}));
645 }
646
647 pub(super) fn persist_config_change(&self, json_pointer: &str, value: serde_json::Value) {
652 let resolver =
653 ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
654 let changes = std::collections::HashMap::from([(json_pointer.to_string(), value)]);
655 let deletions = std::collections::HashSet::new();
656 if let Err(e) = resolver.save_changes_to_layer(&changes, &deletions, ConfigLayer::User) {
657 tracing::error!("Failed to persist config change {}: {}", json_pointer, e);
658 }
659 }
660}