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