par_term/app/
window_state.rs

1//! Per-window state for multi-window terminal emulator
2//!
3//! This module contains `WindowState`, which holds all state specific to a single window,
4//! including its renderer, tab manager, input handler, and UI components.
5
6use crate::app::debug_state::DebugState;
7use crate::clipboard_history_ui::{ClipboardHistoryAction, ClipboardHistoryUI};
8use crate::config::Config;
9use crate::help_ui::HelpUI;
10use crate::input::InputHandler;
11use crate::renderer::Renderer;
12use crate::selection::SelectionMode;
13use crate::settings_ui::{CursorShaderEditorResult, SettingsUI, ShaderEditorResult};
14use crate::tab::TabManager;
15use crate::tab_bar_ui::{TabBarAction, TabBarUI};
16use anyhow::Result;
17use std::sync::Arc;
18use tokio::runtime::Runtime;
19use wgpu::SurfaceError;
20use winit::event::KeyEvent;
21use winit::window::Window;
22
23/// Per-window state that manages a single terminal window with multiple tabs
24pub struct WindowState {
25    pub(crate) config: Config,
26    pub(crate) window: Option<Arc<Window>>,
27    pub(crate) renderer: Option<Renderer>,
28    pub(crate) input_handler: InputHandler,
29    pub(crate) runtime: Arc<Runtime>,
30
31    /// Tab manager for handling multiple terminal tabs
32    pub(crate) tab_manager: TabManager,
33    /// Tab bar UI
34    pub(crate) tab_bar_ui: TabBarUI,
35
36    pub(crate) debug: DebugState,
37
38    /// Cursor opacity for smooth fade animation (0.0 = invisible, 1.0 = fully visible)
39    pub(crate) cursor_opacity: f32,
40    /// Time of last cursor blink toggle
41    pub(crate) last_cursor_blink: Option<std::time::Instant>,
42    /// Time of last key press (to reset cursor blink)
43    pub(crate) last_key_press: Option<std::time::Instant>,
44    /// Whether window is currently in fullscreen mode
45    pub(crate) is_fullscreen: bool,
46    /// egui context for GUI rendering
47    pub(crate) egui_ctx: Option<egui::Context>,
48    /// egui-winit state for event handling
49    pub(crate) egui_state: Option<egui_winit::State>,
50    /// Settings UI manager
51    pub(crate) settings_ui: SettingsUI,
52    /// Help UI manager
53    pub(crate) help_ui: HelpUI,
54    /// Clipboard history UI manager
55    pub(crate) clipboard_history_ui: ClipboardHistoryUI,
56    /// Whether terminal session recording is active
57    pub(crate) is_recording: bool,
58    /// When recording started
59    #[allow(dead_code)]
60    pub(crate) recording_start_time: Option<std::time::Instant>,
61    /// Flag to indicate shutdown is in progress
62    pub(crate) is_shutting_down: bool,
63
64    // Smart redraw tracking (event-driven rendering)
65    /// Whether we need to render next frame
66    pub(crate) needs_redraw: bool,
67    /// When to blink cursor next
68    pub(crate) cursor_blink_timer: Option<std::time::Instant>,
69    /// Whether we need to rebuild renderer after font-related changes
70    pub(crate) pending_font_rebuild: bool,
71}
72
73impl WindowState {
74    /// Create a new window state with the given configuration
75    pub fn new(config: Config, runtime: Arc<Runtime>) -> Self {
76        let settings_ui = SettingsUI::new(config.clone());
77
78        Self {
79            config,
80            window: None,
81            renderer: None,
82            input_handler: InputHandler::new(),
83            runtime,
84
85            tab_manager: TabManager::new(),
86            tab_bar_ui: TabBarUI::new(),
87
88            debug: DebugState::new(),
89
90            cursor_opacity: 1.0,
91            last_cursor_blink: None,
92            last_key_press: None,
93            is_fullscreen: false,
94            egui_ctx: None,
95            egui_state: None,
96            settings_ui,
97            help_ui: HelpUI::new(),
98            clipboard_history_ui: ClipboardHistoryUI::new(),
99            is_recording: false,
100            recording_start_time: None,
101            is_shutting_down: false,
102
103            needs_redraw: true,
104            cursor_blink_timer: None,
105            pending_font_rebuild: false,
106        }
107    }
108
109    // ========================================================================
110    // Tab Management Methods
111    // ========================================================================
112
113    /// Create a new tab
114    pub fn new_tab(&mut self) {
115        // Check max tabs limit
116        if self.config.max_tabs > 0 && self.tab_manager.tab_count() >= self.config.max_tabs {
117            log::warn!(
118                "Cannot create new tab: max_tabs limit ({}) reached",
119                self.config.max_tabs
120            );
121            return;
122        }
123
124        match self.tab_manager.new_tab(
125            &self.config,
126            Arc::clone(&self.runtime),
127            self.config.tab_inherit_cwd,
128        ) {
129            Ok(tab_id) => {
130                // Start refresh task for the new tab and resize to match window
131                if let Some(window) = &self.window
132                    && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
133                {
134                    tab.start_refresh_task(
135                        Arc::clone(&self.runtime),
136                        Arc::clone(window),
137                        self.config.max_fps,
138                    );
139
140                    // Resize terminal to match current renderer dimensions
141                    if let Some(renderer) = &self.renderer
142                        && let Ok(mut term) = tab.terminal.try_lock()
143                    {
144                        let (cols, rows) = renderer.grid_size();
145                        let size = renderer.size();
146                        let width_px = size.width as usize;
147                        let height_px = size.height as usize;
148
149                        // Set cell dimensions
150                        term.set_cell_dimensions(
151                            renderer.cell_width() as u32,
152                            renderer.cell_height() as u32,
153                        );
154
155                        // Resize terminal to match window size
156                        let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
157                        log::info!(
158                            "Resized new tab {} terminal to {}x{} ({}x{} px)",
159                            tab_id,
160                            cols,
161                            rows,
162                            width_px,
163                            height_px
164                        );
165                    }
166                }
167
168                self.needs_redraw = true;
169                if let Some(window) = &self.window {
170                    window.request_redraw();
171                }
172            }
173            Err(e) => {
174                log::error!("Failed to create new tab: {}", e);
175            }
176        }
177    }
178
179    /// Close the current tab
180    /// Returns true if the window should close (last tab was closed)
181    pub fn close_current_tab(&mut self) -> bool {
182        if let Some(tab_id) = self.tab_manager.active_tab_id() {
183            let is_last = self.tab_manager.close_tab(tab_id);
184            self.needs_redraw = true;
185            if let Some(window) = &self.window {
186                window.request_redraw();
187            }
188            is_last
189        } else {
190            true // No tabs, window should close
191        }
192    }
193
194    /// Switch to next tab
195    pub fn next_tab(&mut self) {
196        self.tab_manager.next_tab();
197        // Clear renderer cells and invalidate cache to ensure clean switch
198        if let Some(renderer) = &mut self.renderer {
199            renderer.clear_all_cells();
200        }
201        if let Some(tab) = self.tab_manager.active_tab_mut() {
202            tab.cache.cells = None;
203        }
204        self.needs_redraw = true;
205        if let Some(window) = &self.window {
206            window.request_redraw();
207        }
208    }
209
210    /// Switch to previous tab
211    pub fn prev_tab(&mut self) {
212        self.tab_manager.prev_tab();
213        // Clear renderer cells and invalidate cache to ensure clean switch
214        if let Some(renderer) = &mut self.renderer {
215            renderer.clear_all_cells();
216        }
217        if let Some(tab) = self.tab_manager.active_tab_mut() {
218            tab.cache.cells = None;
219        }
220        self.needs_redraw = true;
221        if let Some(window) = &self.window {
222            window.request_redraw();
223        }
224    }
225
226    /// Switch to tab by index (1-based)
227    pub fn switch_to_tab_index(&mut self, index: usize) {
228        self.tab_manager.switch_to_index(index);
229        // Clear renderer cells and invalidate cache to ensure clean switch
230        if let Some(renderer) = &mut self.renderer {
231            renderer.clear_all_cells();
232        }
233        if let Some(tab) = self.tab_manager.active_tab_mut() {
234            tab.cache.cells = None;
235        }
236        self.needs_redraw = true;
237        if let Some(window) = &self.window {
238            window.request_redraw();
239        }
240    }
241
242    /// Move current tab left
243    pub fn move_tab_left(&mut self) {
244        self.tab_manager.move_active_tab_left();
245        self.needs_redraw = true;
246        if let Some(window) = &self.window {
247            window.request_redraw();
248        }
249    }
250
251    /// Move current tab right
252    pub fn move_tab_right(&mut self) {
253        self.tab_manager.move_active_tab_right();
254        self.needs_redraw = true;
255        if let Some(window) = &self.window {
256            window.request_redraw();
257        }
258    }
259
260    /// Duplicate current tab
261    pub fn duplicate_tab(&mut self) {
262        match self
263            .tab_manager
264            .duplicate_active_tab(&self.config, Arc::clone(&self.runtime))
265        {
266            Ok(Some(tab_id)) => {
267                // Start refresh task for the new tab
268                if let Some(window) = &self.window
269                    && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
270                {
271                    tab.start_refresh_task(
272                        Arc::clone(&self.runtime),
273                        Arc::clone(window),
274                        self.config.max_fps,
275                    );
276                }
277                self.needs_redraw = true;
278                if let Some(window) = &self.window {
279                    window.request_redraw();
280                }
281            }
282            Ok(None) => {
283                log::debug!("No active tab to duplicate");
284            }
285            Err(e) => {
286                log::error!("Failed to duplicate tab: {}", e);
287            }
288        }
289    }
290
291    /// Check if there are multiple tabs
292    pub fn has_multiple_tabs(&self) -> bool {
293        self.tab_manager.has_multiple_tabs()
294    }
295
296    /// Get the active tab's terminal
297    #[allow(dead_code)]
298    pub fn active_terminal(
299        &self,
300    ) -> Option<&Arc<tokio::sync::Mutex<crate::terminal::TerminalManager>>> {
301        self.tab_manager.active_tab().map(|tab| &tab.terminal)
302    }
303
304    // ========================================================================
305    // Active Tab State Accessors (compatibility - may be useful later)
306    // ========================================================================
307    #[allow(dead_code)]
308    pub(crate) fn terminal(
309        &self,
310    ) -> Option<&Arc<tokio::sync::Mutex<crate::terminal::TerminalManager>>> {
311        self.active_terminal()
312    }
313
314    #[allow(dead_code)]
315    pub(crate) fn scroll_state(&self) -> Option<&crate::scroll_state::ScrollState> {
316        self.tab_manager.active_tab().map(|t| &t.scroll_state)
317    }
318
319    #[allow(dead_code)]
320    pub(crate) fn scroll_state_mut(&mut self) -> Option<&mut crate::scroll_state::ScrollState> {
321        self.tab_manager
322            .active_tab_mut()
323            .map(|t| &mut t.scroll_state)
324    }
325
326    #[allow(dead_code)]
327    pub(crate) fn mouse(&self) -> Option<&crate::app::mouse::MouseState> {
328        self.tab_manager.active_tab().map(|t| &t.mouse)
329    }
330
331    #[allow(dead_code)]
332    pub(crate) fn mouse_mut(&mut self) -> Option<&mut crate::app::mouse::MouseState> {
333        self.tab_manager.active_tab_mut().map(|t| &mut t.mouse)
334    }
335
336    #[allow(dead_code)]
337    pub(crate) fn bell(&self) -> Option<&crate::app::bell::BellState> {
338        self.tab_manager.active_tab().map(|t| &t.bell)
339    }
340
341    #[allow(dead_code)]
342    pub(crate) fn bell_mut(&mut self) -> Option<&mut crate::app::bell::BellState> {
343        self.tab_manager.active_tab_mut().map(|t| &mut t.bell)
344    }
345
346    #[allow(dead_code)]
347    pub(crate) fn cache(&self) -> Option<&crate::app::render_cache::RenderCache> {
348        self.tab_manager.active_tab().map(|t| &t.cache)
349    }
350
351    #[allow(dead_code)]
352    pub(crate) fn cache_mut(&mut self) -> Option<&mut crate::app::render_cache::RenderCache> {
353        self.tab_manager.active_tab_mut().map(|t| &mut t.cache)
354    }
355
356    #[allow(dead_code)]
357    pub(crate) fn refresh_task(&self) -> Option<&Option<tokio::task::JoinHandle<()>>> {
358        self.tab_manager.active_tab().map(|t| &t.refresh_task)
359    }
360
361    #[allow(dead_code)]
362    pub(crate) fn abort_refresh_task(&mut self) {
363        if let Some(tab) = self.tab_manager.active_tab_mut()
364            && let Some(task) = tab.refresh_task.take()
365        {
366            task.abort();
367        }
368    }
369
370    /// Extract a substring based on character columns to avoid UTF-8 slicing panics
371    pub(crate) fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
372        let mut extracted = String::new();
373        let end_bound = end_col.unwrap_or(usize::MAX);
374
375        if start_col > end_bound {
376            return extracted;
377        }
378
379        for (idx, ch) in line.chars().enumerate() {
380            if idx > end_bound {
381                break;
382            }
383
384            if idx >= start_col {
385                extracted.push(ch);
386            }
387        }
388
389        extracted
390    }
391
392    /// Rebuild the renderer after font-related changes and resize the terminal accordingly
393    pub(crate) fn rebuild_renderer(&mut self) -> Result<()> {
394        let window = if let Some(w) = &self.window {
395            Arc::clone(w)
396        } else {
397            return Ok(()); // Nothing to rebuild yet
398        };
399
400        let theme = self.config.load_theme();
401        let font_family = if self.config.font_family.is_empty() {
402            None
403        } else {
404            Some(self.config.font_family.as_str())
405        };
406
407        let mut renderer = self.runtime.block_on(Renderer::new(
408            Arc::clone(&window),
409            font_family,
410            self.config.font_family_bold.as_deref(),
411            self.config.font_family_italic.as_deref(),
412            self.config.font_family_bold_italic.as_deref(),
413            &self.config.font_ranges,
414            self.config.font_size,
415            self.config.window_padding,
416            self.config.line_spacing,
417            self.config.char_spacing,
418            &self.config.scrollbar_position,
419            self.config.scrollbar_width,
420            self.config.scrollbar_thumb_color,
421            self.config.scrollbar_track_color,
422            self.config.enable_text_shaping,
423            self.config.enable_ligatures,
424            self.config.enable_kerning,
425            self.config.vsync_mode,
426            self.config.window_opacity,
427            theme.background.as_array(),
428            self.config.background_image.as_deref(),
429            self.config.background_image_enabled,
430            self.config.background_image_mode,
431            self.config.background_image_opacity,
432            self.config.custom_shader.as_deref(),
433            self.config.custom_shader_enabled,
434            self.config.custom_shader_animation,
435            self.config.custom_shader_animation_speed,
436            self.config.custom_shader_text_opacity,
437            self.config.custom_shader_full_content,
438            self.config.custom_shader_brightness,
439            // Custom shader channel textures
440            &self.config.shader_channel_paths(),
441            // Cursor shader settings
442            self.config.cursor_shader.as_deref(),
443            self.config.cursor_shader_enabled,
444            self.config.cursor_shader_animation,
445            self.config.cursor_shader_animation_speed,
446        ))?;
447
448        let (cols, rows) = renderer.grid_size();
449        let cell_width = renderer.cell_width();
450        let cell_height = renderer.cell_height();
451        let width_px = (cols as f32 * cell_width) as usize;
452        let height_px = (rows as f32 * cell_height) as usize;
453
454        // Resize all tabs' terminals
455        for tab in self.tab_manager.tabs_mut() {
456            if let Ok(mut term) = tab.terminal.try_lock() {
457                let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
458                term.set_cell_dimensions(cell_width as u32, cell_height as u32);
459                term.set_theme(self.config.load_theme());
460            }
461            tab.cache.cells = None;
462        }
463
464        // Initialize cursor shader config
465        renderer.update_cursor_shader_config(
466            self.config.cursor_shader_color,
467            self.config.cursor_shader_trail_duration,
468            self.config.cursor_shader_glow_radius,
469            self.config.cursor_shader_glow_intensity,
470        );
471
472        // Initialize cursor color from config
473        renderer.update_cursor_color(self.config.cursor_color);
474
475        // Hide cursor if cursor shader is enabled and configured to hide
476        renderer.set_cursor_hidden_for_shader(
477            self.config.cursor_shader_enabled && self.config.cursor_shader_hides_cursor,
478        );
479
480        self.renderer = Some(renderer);
481        self.needs_redraw = true;
482
483        // Reset egui GPU textures so the new renderer has a fresh atlas, but
484        // preserve window positions/collapse state by cloning the previous
485        // egui memory into the new context (otherwise the Settings window
486        // snaps to the top-left and all panels collapse after font changes).
487        let previous_memory = self
488            .egui_ctx
489            .as_ref()
490            .map(|ctx| ctx.memory(|mem| mem.clone()));
491
492        let scale_factor = window.scale_factor() as f32;
493        let egui_ctx = egui::Context::default();
494        if let Some(memory) = previous_memory {
495            egui_ctx.memory_mut(|mem| *mem = memory);
496        }
497        let egui_state = egui_winit::State::new(
498            egui_ctx.clone(),
499            egui::ViewportId::ROOT,
500            &window,
501            Some(scale_factor),
502            None,
503            None,
504        );
505        self.egui_ctx = Some(egui_ctx);
506        self.egui_state = Some(egui_state);
507
508        if let Some(window) = &self.window {
509            window.request_redraw();
510        }
511
512        Ok(())
513    }
514
515    /// Initialize the window asynchronously
516    pub(crate) async fn initialize_async(&mut self, window: Window) -> Result<()> {
517        // Enable IME (Input Method Editor) to receive all character events including Space
518        window.set_ime_allowed(true);
519        log::debug!("IME enabled for character input");
520
521        let window = Arc::new(window);
522
523        // Initialize egui context and state
524        let egui_ctx = egui::Context::default();
525        let egui_state = egui_winit::State::new(
526            egui_ctx.clone(),
527            egui::ViewportId::ROOT,
528            &window,
529            Some(window.scale_factor() as f32),
530            None,
531            None, // max_texture_side
532        );
533        self.egui_ctx = Some(egui_ctx);
534        self.egui_state = Some(egui_state);
535
536        // Create renderer with font family from config
537        let font_family = if self.config.font_family.is_empty() {
538            None
539        } else {
540            Some(self.config.font_family.as_str())
541        };
542        let theme = self.config.load_theme();
543        let mut renderer = Renderer::new(
544            Arc::clone(&window),
545            font_family,
546            self.config.font_family_bold.as_deref(),
547            self.config.font_family_italic.as_deref(),
548            self.config.font_family_bold_italic.as_deref(),
549            &self.config.font_ranges,
550            self.config.font_size,
551            self.config.window_padding,
552            self.config.line_spacing,
553            self.config.char_spacing,
554            &self.config.scrollbar_position,
555            self.config.scrollbar_width,
556            self.config.scrollbar_thumb_color,
557            self.config.scrollbar_track_color,
558            self.config.enable_text_shaping,
559            self.config.enable_ligatures,
560            self.config.enable_kerning,
561            self.config.vsync_mode,
562            self.config.window_opacity,
563            theme.background.as_array(),
564            self.config.background_image.as_deref(),
565            self.config.background_image_enabled,
566            self.config.background_image_mode,
567            self.config.background_image_opacity,
568            self.config.custom_shader.as_deref(),
569            self.config.custom_shader_enabled,
570            self.config.custom_shader_animation,
571            self.config.custom_shader_animation_speed,
572            self.config.custom_shader_text_opacity,
573            self.config.custom_shader_full_content,
574            self.config.custom_shader_brightness,
575            // Custom shader channel textures
576            &self.config.shader_channel_paths(),
577            // Cursor shader settings
578            self.config.cursor_shader.as_deref(),
579            self.config.cursor_shader_enabled,
580            self.config.cursor_shader_animation,
581            self.config.cursor_shader_animation_speed,
582        )
583        .await?;
584
585        // macOS: also update the NSWindow alpha so the OS compositor reflects live opacity
586        // macOS: Configure CAMetalLayer (transparency + performance)
587        // This MUST be done AFTER creating the wgpu surface/renderer
588        // so that the CAMetalLayer has been created by wgpu
589        #[cfg(target_os = "macos")]
590        {
591            if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
592                log::warn!("Failed to configure Metal layer: {}", e);
593                log::warn!(
594                    "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
595                );
596            }
597            // Set initial layer opacity to match config (content only, frame unaffected)
598            if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
599                log::warn!("Failed to set initial Metal layer opacity: {}", e);
600            }
601        }
602
603        // Initialize cursor shader config
604        renderer.update_cursor_shader_config(
605            self.config.cursor_shader_color,
606            self.config.cursor_shader_trail_duration,
607            self.config.cursor_shader_glow_radius,
608            self.config.cursor_shader_glow_intensity,
609        );
610
611        // Initialize cursor color from config
612        renderer.update_cursor_color(self.config.cursor_color);
613
614        // Hide cursor if cursor shader is enabled and configured to hide
615        renderer.set_cursor_hidden_for_shader(
616            self.config.cursor_shader_enabled && self.config.cursor_shader_hides_cursor,
617        );
618
619        self.window = Some(Arc::clone(&window));
620        self.renderer = Some(renderer);
621
622        // Create the first tab
623        let tab_id = self.tab_manager.new_tab(
624            &self.config,
625            Arc::clone(&self.runtime),
626            false, // First tab doesn't inherit cwd
627        )?;
628
629        // Resize the tab's terminal to match renderer grid
630        if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
631            if let Some(renderer) = &self.renderer {
632                let (renderer_cols, renderer_rows) = renderer.grid_size();
633                let cell_width = renderer.cell_width();
634                let cell_height = renderer.cell_height();
635                let width_px = (renderer_cols as f32 * cell_width) as usize;
636                let height_px = (renderer_rows as f32 * cell_height) as usize;
637
638                if let Ok(mut term) = tab.terminal.try_lock() {
639                    let _ =
640                        term.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px);
641                    term.set_cell_dimensions(cell_width as u32, cell_height as u32);
642                    log::info!(
643                        "Initial terminal dimensions: {}x{} ({}x{} px)",
644                        renderer_cols,
645                        renderer_rows,
646                        width_px,
647                        height_px
648                    );
649                }
650            }
651
652            // Start refresh task for the first tab
653            tab.start_refresh_task(
654                Arc::clone(&self.runtime),
655                Arc::clone(&window),
656                self.config.max_fps,
657            );
658        }
659
660        Ok(())
661    }
662
663    /// Force surface reconfiguration - useful when rendering becomes corrupted
664    /// after moving between monitors or when automatic detection fails.
665    /// Also clears glyph cache to ensure fonts render correctly.
666    pub(crate) fn force_surface_reconfigure(&mut self) {
667        log::info!("Force surface reconfigure triggered");
668
669        if let Some(renderer) = &mut self.renderer {
670            // Reconfigure the surface
671            renderer.reconfigure_surface();
672
673            // Clear glyph cache to force re-rasterization at correct DPI
674            renderer.clear_glyph_cache();
675
676            // Invalidate cached cells to force full re-render
677            if let Some(tab) = self.tab_manager.active_tab_mut() {
678                tab.cache.cells = None;
679            }
680        }
681
682        // On macOS, reconfigure the Metal layer
683        #[cfg(target_os = "macos")]
684        {
685            if let Some(window) = &self.window
686                && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
687            {
688                log::warn!("Failed to reconfigure Metal layer: {}", e);
689            }
690        }
691
692        // Request redraw
693        if let Some(window) = &self.window {
694            window.request_redraw();
695        }
696
697        self.needs_redraw = true;
698    }
699
700    pub(crate) fn handle_fullscreen_toggle(&mut self, event: &KeyEvent) -> bool {
701        use winit::event::ElementState;
702        use winit::keyboard::{Key, NamedKey};
703
704        if event.state != ElementState::Pressed {
705            return false;
706        }
707
708        // F11: Toggle fullscreen
709        if matches!(event.logical_key, Key::Named(NamedKey::F11))
710            && let Some(window) = &self.window
711        {
712            self.is_fullscreen = !self.is_fullscreen;
713
714            if self.is_fullscreen {
715                window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
716                log::info!("Entering fullscreen mode");
717            } else {
718                window.set_fullscreen(None);
719                log::info!("Exiting fullscreen mode");
720            }
721
722            return true;
723        }
724
725        false
726    }
727
728    pub(crate) fn handle_settings_toggle(&mut self, event: &KeyEvent) -> bool {
729        use winit::event::ElementState;
730        use winit::keyboard::{Key, NamedKey};
731
732        if event.state != ElementState::Pressed {
733            return false;
734        }
735
736        // F12: Toggle settings UI
737        if matches!(event.logical_key, Key::Named(NamedKey::F12)) {
738            self.settings_ui.toggle();
739            log::info!(
740                "Settings UI toggled: {}",
741                if self.settings_ui.visible {
742                    "visible"
743                } else {
744                    "hidden"
745                }
746            );
747
748            // Request redraw to show/hide settings
749            if let Some(window) = &self.window {
750                window.request_redraw();
751            }
752
753            return true;
754        }
755
756        false
757    }
758
759    /// Handle F1 key to toggle help panel
760    pub(crate) fn handle_help_toggle(&mut self, event: &KeyEvent) -> bool {
761        use winit::event::ElementState;
762        use winit::keyboard::{Key, NamedKey};
763
764        if event.state != ElementState::Pressed {
765            return false;
766        }
767
768        // F1: Toggle help UI
769        if matches!(event.logical_key, Key::Named(NamedKey::F1)) {
770            self.help_ui.toggle();
771            log::info!(
772                "Help UI toggled: {}",
773                if self.help_ui.visible {
774                    "visible"
775                } else {
776                    "hidden"
777                }
778            );
779
780            // Request redraw to show/hide help
781            if let Some(window) = &self.window {
782                window.request_redraw();
783            }
784
785            return true;
786        }
787
788        // Escape: Close help UI if visible
789        if matches!(event.logical_key, Key::Named(NamedKey::Escape)) && self.help_ui.visible {
790            self.help_ui.visible = false;
791            log::info!("Help UI closed via Escape");
792
793            if let Some(window) = &self.window {
794                window.request_redraw();
795            }
796
797            return true;
798        }
799
800        false
801    }
802
803    /// Handle F11 key to toggle shader editor
804    pub(crate) fn handle_shader_editor_toggle(&mut self, event: &KeyEvent) -> bool {
805        use winit::event::ElementState;
806        use winit::keyboard::{Key, NamedKey};
807
808        if event.state != ElementState::Pressed {
809            return false;
810        }
811
812        // F11: Toggle shader editor
813        if matches!(event.logical_key, Key::Named(NamedKey::F11)) {
814            if self.settings_ui.is_shader_editor_visible() {
815                // Close shader editor - handled by the UI itself
816                log::info!("Shader editor close requested via F11");
817            } else {
818                // Open shader editor
819                if self.settings_ui.open_shader_editor() {
820                    log::info!("Shader editor opened via F11");
821                } else {
822                    log::warn!("Cannot open shader editor: no shader path configured in settings");
823                }
824            }
825
826            // Request redraw to show/hide shader editor
827            if let Some(window) = &self.window {
828                window.request_redraw();
829            }
830
831            return true;
832        }
833
834        false
835    }
836
837    /// Handle F3 key to toggle FPS overlay
838    pub(crate) fn handle_fps_overlay_toggle(&mut self, event: &KeyEvent) -> bool {
839        use winit::event::ElementState;
840        use winit::keyboard::{Key, NamedKey};
841
842        if event.state != ElementState::Pressed {
843            return false;
844        }
845
846        // F3: Toggle FPS overlay
847        if matches!(event.logical_key, Key::Named(NamedKey::F3)) {
848            self.debug.show_fps_overlay = !self.debug.show_fps_overlay;
849            log::info!(
850                "FPS overlay toggled: {}",
851                if self.debug.show_fps_overlay {
852                    "visible"
853                } else {
854                    "hidden"
855                }
856            );
857
858            // Request redraw to show/hide FPS overlay
859            if let Some(window) = &self.window {
860                window.request_redraw();
861            }
862
863            return true;
864        }
865
866        false
867    }
868
869    pub(crate) fn scroll_up_page(&mut self) {
870        // Calculate page size based on visible lines
871        let (target_offset, scrollback_len) = {
872            let tab = if let Some(t) = self.tab_manager.active_tab() {
873                t
874            } else {
875                return;
876            };
877            (tab.scroll_state.target_offset, tab.cache.scrollback_len)
878        };
879
880        if let Some(renderer) = &self.renderer {
881            let char_height = self.config.font_size * 1.2;
882            let page_size = (renderer.size().height as f32 / char_height) as usize;
883
884            let new_target = target_offset.saturating_add(page_size);
885            let clamped_target = new_target.min(scrollback_len);
886            self.set_scroll_target(clamped_target);
887        }
888    }
889
890    pub(crate) fn scroll_down_page(&mut self) {
891        // Calculate page size based on visible lines
892        let target_offset = {
893            if let Some(tab) = self.tab_manager.active_tab() {
894                tab.scroll_state.target_offset
895            } else {
896                return;
897            }
898        };
899
900        if let Some(renderer) = &self.renderer {
901            let char_height = self.config.font_size * 1.2;
902            let page_size = (renderer.size().height as f32 / char_height) as usize;
903
904            let new_target = target_offset.saturating_sub(page_size);
905            self.set_scroll_target(new_target);
906        }
907    }
908
909    pub(crate) fn scroll_to_top(&mut self) {
910        let scrollback_len = {
911            if let Some(tab) = self.tab_manager.active_tab() {
912                tab.cache.scrollback_len
913            } else {
914                return;
915            }
916        };
917        self.set_scroll_target(scrollback_len);
918    }
919
920    pub(crate) fn scroll_to_bottom(&mut self) {
921        self.set_scroll_target(0);
922    }
923
924    /// Check if egui is currently using the pointer (mouse is over an egui UI element)
925    pub(crate) fn is_egui_using_pointer(&self) -> bool {
926        // If any UI panel is visible, check if egui wants the pointer
927        let any_ui_visible =
928            self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
929        if !any_ui_visible {
930            return false;
931        }
932
933        // Check egui context for pointer usage
934        if let Some(ctx) = &self.egui_ctx {
935            ctx.is_using_pointer() || ctx.wants_pointer_input()
936        } else {
937            false
938        }
939    }
940
941    /// Check if egui is currently using keyboard input (e.g., text input or ComboBox has focus)
942    pub(crate) fn is_egui_using_keyboard(&self) -> bool {
943        // If any UI panel is visible, check if egui wants keyboard input
944        let any_ui_visible =
945            self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
946        if !any_ui_visible {
947            return false;
948        }
949
950        // Check egui context for keyboard usage
951        if let Some(ctx) = &self.egui_ctx {
952            ctx.wants_keyboard_input()
953        } else {
954            false
955        }
956    }
957
958    /// Determine if scrollbar should be visible based on autohide setting and recent activity
959    pub(crate) fn should_show_scrollbar(&self) -> bool {
960        let tab = match self.tab_manager.active_tab() {
961            Some(t) => t,
962            None => return false,
963        };
964
965        // No scrollbar needed if no scrollback available
966        if tab.cache.scrollback_len == 0 {
967            return false;
968        }
969
970        // Always show when dragging or moving
971        if tab.scroll_state.dragging {
972            return true;
973        }
974
975        // If autohide disabled, always show
976        if self.config.scrollbar_autohide_delay == 0 {
977            return true;
978        }
979
980        // If scrolled away from bottom, keep visible
981        if tab.scroll_state.offset > 0 || tab.scroll_state.target_offset > 0 {
982            return true;
983        }
984
985        // Show when pointer is near the scrollbar edge (hover reveal)
986        if let Some(window) = &self.window {
987            let padding = 32.0; // px hover band
988            let width = window.inner_size().width as f64;
989            let near_right = self.config.scrollbar_position != "left"
990                && (width - tab.mouse.position.0) <= padding;
991            let near_left =
992                self.config.scrollbar_position == "left" && tab.mouse.position.0 <= padding;
993            if near_left || near_right {
994                return true;
995            }
996        }
997
998        // Otherwise, hide after delay
999        tab.scroll_state.last_activity.elapsed().as_millis()
1000            < self.config.scrollbar_autohide_delay as u128
1001    }
1002
1003    /// Update cursor blink state based on configured interval
1004    pub(crate) fn update_cursor_blink(&mut self) {
1005        if !self.config.cursor_blink {
1006            // Smoothly fade to full visibility if blinking disabled
1007            self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1008            return;
1009        }
1010
1011        let now = std::time::Instant::now();
1012
1013        // If key was pressed recently (within 500ms), smoothly fade in cursor and reset blink timer
1014        if let Some(last_key) = self.last_key_press
1015            && now.duration_since(last_key).as_millis() < 500
1016        {
1017            self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1018            self.last_cursor_blink = Some(now);
1019            return;
1020        }
1021
1022        // Smooth cursor blink animation using sine wave for natural fade
1023        let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
1024
1025        if let Some(last_blink) = self.last_cursor_blink {
1026            let elapsed = now.duration_since(last_blink);
1027            let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
1028
1029            // Use cosine wave for smooth fade in/out (starts at 1.0, fades to 0.0, back to 1.0)
1030            self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
1031                .abs()
1032                .clamp(0.0, 1.0);
1033
1034            // Reset timer after full cycle (2x interval for full on+off)
1035            if elapsed >= blink_interval * 2 {
1036                self.last_cursor_blink = Some(now);
1037            }
1038        } else {
1039            // First time, start the blink timer with cursor fully visible
1040            self.cursor_opacity = 1.0;
1041            self.last_cursor_blink = Some(now);
1042        }
1043    }
1044
1045    /// Main render function for this window
1046    pub(crate) fn render(&mut self) {
1047        // Skip rendering if shutting down
1048        if self.is_shutting_down {
1049            return;
1050        }
1051
1052        let absolute_start = std::time::Instant::now();
1053
1054        // Reset redraw flag after rendering
1055        // This flag will be set again in about_to_wait if another redraw is needed
1056        self.needs_redraw = false;
1057
1058        // Track frame timing
1059        let frame_start = std::time::Instant::now();
1060
1061        // Calculate frame time from last render
1062        if let Some(last_start) = self.debug.last_frame_start {
1063            let frame_time = frame_start.duration_since(last_start);
1064            self.debug.frame_times.push(frame_time);
1065            if self.debug.frame_times.len() > 60 {
1066                self.debug.frame_times.remove(0);
1067            }
1068        }
1069        self.debug.last_frame_start = Some(frame_start);
1070
1071        // Update scroll animation
1072        let animation_running = if let Some(tab) = self.tab_manager.active_tab_mut() {
1073            tab.scroll_state.update_animation()
1074        } else {
1075            false
1076        };
1077
1078        // Update tab titles from terminal OSC sequences
1079        self.tab_manager.update_all_titles();
1080
1081        // Rebuild renderer if font-related settings changed
1082        if self.pending_font_rebuild {
1083            if let Err(e) = self.rebuild_renderer() {
1084                log::error!("Failed to rebuild renderer after font change: {}", e);
1085            }
1086            self.pending_font_rebuild = false;
1087        }
1088
1089        let (renderer_size, visible_lines) = if let Some(renderer) = &self.renderer {
1090            (renderer.size(), renderer.grid_size().1)
1091        } else {
1092            return;
1093        };
1094
1095        // Get active tab's terminal
1096        let tab = match self.tab_manager.active_tab() {
1097            Some(t) => t,
1098            None => return,
1099        };
1100        let terminal = &tab.terminal;
1101
1102        // Check if shell has exited
1103        let _is_running = if let Ok(term) = terminal.try_lock() {
1104            term.is_running()
1105        } else {
1106            true // Assume running if locked
1107        };
1108
1109        // Request another redraw if animation is still running
1110        if animation_running && let Some(window) = &self.window {
1111            window.request_redraw();
1112        }
1113
1114        // Get scroll offset and selection from active tab
1115        let scroll_offset = tab.scroll_state.offset;
1116        let mouse_selection = tab.mouse.selection;
1117
1118        // Get terminal cells for rendering (with dirty tracking optimization)
1119        let (cells, current_cursor_pos, cursor_style) = if let Ok(term) = terminal.try_lock() {
1120            // Get current generation to check if terminal content has changed
1121            let current_generation = term.update_generation();
1122
1123            // Normalize selection if it exists and extract mode
1124            let (selection, rectangular) = if let Some(sel) = mouse_selection {
1125                (
1126                    Some(sel.normalized()),
1127                    sel.mode == SelectionMode::Rectangular,
1128                )
1129            } else {
1130                (None, false)
1131            };
1132
1133            // Get cursor position and opacity (only show if we're at the bottom with no scroll offset
1134            // and the cursor is visible - TUI apps hide cursor via DECTCEM escape sequence)
1135            let current_cursor_pos = if scroll_offset == 0 && term.is_cursor_visible() {
1136                Some(term.cursor_position())
1137            } else {
1138                None
1139            };
1140
1141            let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
1142
1143            // Get cursor style for geometric rendering
1144            let cursor_style = if current_cursor_pos.is_some() {
1145                Some(term.cursor_style())
1146            } else {
1147                None
1148            };
1149
1150            log::trace!(
1151                "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
1152                current_cursor_pos,
1153                self.cursor_opacity,
1154                cursor_style,
1155                scroll_offset,
1156                term.is_cursor_visible()
1157            );
1158
1159            // Check if we need to regenerate cells
1160            // Only regenerate when content actually changes, not on every cursor blink
1161            let needs_regeneration = tab.cache.cells.is_none()
1162                || current_generation != tab.cache.generation
1163                || scroll_offset != tab.cache.scroll_offset
1164                || current_cursor_pos != tab.cache.cursor_pos // Regenerate if cursor position changed
1165                || mouse_selection != tab.cache.selection; // Regenerate if selection changed (including clearing)
1166
1167            let cell_gen_start = std::time::Instant::now();
1168            let (cells, is_cache_hit) = if needs_regeneration {
1169                // Generate fresh cells
1170                let fresh_cells =
1171                    term.get_cells_with_scrollback(scroll_offset, selection, rectangular, cursor);
1172
1173                (fresh_cells, false)
1174            } else {
1175                // Use cached cells - clone is still needed because of apply_url_underlines
1176                // but we track it accurately for debug logging
1177                (tab.cache.cells.as_ref().unwrap().clone(), true)
1178            };
1179            self.debug.cache_hit = is_cache_hit;
1180            self.debug.cell_gen_time = cell_gen_start.elapsed();
1181
1182            (cells, current_cursor_pos, cursor_style)
1183        } else {
1184            return; // Terminal locked, skip this frame
1185        };
1186
1187        // Update cache with regenerated cells (if needed)
1188        // Need to re-borrow as mutable after the terminal lock is released
1189        if !self.debug.cache_hit
1190            && let Some(tab) = self.tab_manager.active_tab_mut()
1191            && let Ok(term) = tab.terminal.try_lock()
1192        {
1193            let current_generation = term.update_generation();
1194            tab.cache.cells = Some(cells.clone());
1195            tab.cache.generation = current_generation;
1196            tab.cache.scroll_offset = tab.scroll_state.offset;
1197            tab.cache.cursor_pos = current_cursor_pos;
1198            tab.cache.selection = tab.mouse.selection;
1199        }
1200
1201        // Get scrollback length and terminal title from terminal
1202        // Note: When terminal width changes during resize, the core library clears
1203        // scrollback because the old cells would be misaligned with the new column count.
1204        // This is a limitation of the current implementation - proper reflow is not yet supported.
1205        let tab = match self.tab_manager.active_tab() {
1206            Some(t) => t,
1207            None => return,
1208        };
1209        let terminal = &tab.terminal;
1210        let cached_scrollback_len = tab.cache.scrollback_len;
1211        let cached_terminal_title = tab.cache.terminal_title.clone();
1212        let hovered_url = tab.mouse.hovered_url.clone();
1213
1214        let (scrollback_len, terminal_title) = if let Ok(term) = terminal.try_lock() {
1215            (term.scrollback_len(), term.get_title())
1216        } else {
1217            (cached_scrollback_len, cached_terminal_title.clone())
1218        };
1219
1220        // Update cache scrollback and clamp scroll state
1221        if let Some(tab) = self.tab_manager.active_tab_mut() {
1222            tab.cache.scrollback_len = scrollback_len;
1223            tab.scroll_state
1224                .clamp_to_scrollback(tab.cache.scrollback_len);
1225        }
1226
1227        // Update window title if terminal has set one via OSC sequences
1228        // Only if allow_title_change is enabled and we're not showing a URL tooltip
1229        if self.config.allow_title_change
1230            && hovered_url.is_none()
1231            && terminal_title != cached_terminal_title
1232        {
1233            if let Some(tab) = self.tab_manager.active_tab_mut() {
1234                tab.cache.terminal_title = terminal_title.clone();
1235            }
1236            if let Some(window) = &self.window {
1237                if terminal_title.is_empty() {
1238                    // Restore configured title when terminal clears title
1239                    window.set_title(&self.config.window_title);
1240                } else {
1241                    // Use terminal-set title
1242                    window.set_title(&terminal_title);
1243                }
1244            }
1245        }
1246
1247        // Total lines = visible lines + actual scrollback content
1248        let total_lines = visible_lines + scrollback_len;
1249
1250        // Detect URLs in visible area (only when content changed)
1251        // This optimization saves ~0.26ms per frame on cache hits
1252        let url_detect_start = std::time::Instant::now();
1253        let debug_url_detect_time = if !self.debug.cache_hit {
1254            // Content changed - re-detect URLs
1255            self.detect_urls();
1256            url_detect_start.elapsed()
1257        } else {
1258            // Content unchanged - use cached URL detection
1259            std::time::Duration::ZERO
1260        };
1261
1262        // Apply URL underlining to cells (always apply, since cells might be regenerated)
1263        let url_underline_start = std::time::Instant::now();
1264        let mut cells = cells; // Make cells mutable
1265        self.apply_url_underlines(&mut cells, &renderer_size);
1266        let _debug_url_underline_time = url_underline_start.elapsed();
1267
1268        // Update cursor blink state
1269        self.update_cursor_blink();
1270
1271        let render_start = std::time::Instant::now();
1272
1273        let mut debug_update_cells_time = std::time::Duration::ZERO;
1274        #[allow(unused_assignments)]
1275        let mut debug_graphics_time = std::time::Duration::ZERO;
1276        #[allow(unused_assignments)]
1277        let mut debug_actual_render_time = std::time::Duration::ZERO;
1278        let _ = &debug_actual_render_time;
1279        // Clipboard action to handle after rendering (declared here to survive renderer borrow)
1280        let mut pending_clipboard_action = ClipboardHistoryAction::None;
1281        // Tab bar action to handle after rendering (declared here to survive renderer borrow)
1282        let mut pending_tab_action = TabBarAction::None;
1283
1284        let show_scrollbar = self.should_show_scrollbar();
1285
1286        if let Some(renderer) = &mut self.renderer {
1287            // Only update renderer with cells if they changed (cache MISS)
1288            // This avoids re-uploading the same cell data to GPU on every frame
1289            if !self.debug.cache_hit {
1290                let t = std::time::Instant::now();
1291                renderer.update_cells(&cells);
1292                debug_update_cells_time = t.elapsed();
1293            }
1294
1295            // Update cursor position and style for geometric rendering
1296            if let (Some(pos), Some(opacity), Some(style)) =
1297                (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
1298            {
1299                renderer.update_cursor(pos, opacity, style);
1300                // Forward cursor state to custom shader for Ghostty-compatible cursor animations
1301                // Use the configured cursor color
1302                let cursor_color = [
1303                    self.config.cursor_color[0] as f32 / 255.0,
1304                    self.config.cursor_color[1] as f32 / 255.0,
1305                    self.config.cursor_color[2] as f32 / 255.0,
1306                    1.0,
1307                ];
1308                renderer.update_shader_cursor(pos.0, pos.1, opacity, cursor_color, style);
1309            } else {
1310                renderer.clear_cursor();
1311            }
1312
1313            // If settings UI is visible, sync app config to UI working copy and push opacity
1314            if self.settings_ui.visible {
1315                let ui_cfg = self.settings_ui.current_config().clone();
1316                if (ui_cfg.window_opacity - self.config.window_opacity).abs() > 1e-4 {
1317                    log::info!(
1318                        "Syncing live opacity from UI {:.3} (app {:.3})",
1319                        ui_cfg.window_opacity,
1320                        self.config.window_opacity
1321                    );
1322                    self.config.window_opacity = ui_cfg.window_opacity;
1323                }
1324
1325                renderer.update_opacity(self.config.window_opacity);
1326                if let Some(tab) = self.tab_manager.active_tab_mut() {
1327                    tab.cache.applied_opacity = self.config.window_opacity;
1328                    tab.cache.cells = None;
1329                }
1330                if let Some(window) = &self.window {
1331                    window.request_redraw();
1332                }
1333            }
1334
1335            // Update scrollbar
1336            let scroll_offset = self
1337                .tab_manager
1338                .active_tab()
1339                .map(|t| t.scroll_state.offset)
1340                .unwrap_or(0);
1341            renderer.update_scrollbar(scroll_offset, visible_lines, total_lines);
1342
1343            // Update animations and request redraw if frames changed
1344            let anim_start = std::time::Instant::now();
1345            if let Some(tab) = self.tab_manager.active_tab() {
1346                let terminal = tab.terminal.blocking_lock();
1347                if terminal.update_animations() {
1348                    // Animation frame changed - request continuous redraws while animations are playing
1349                    if let Some(window) = &self.window {
1350                        window.request_redraw();
1351                    }
1352                }
1353            }
1354            let debug_anim_time = anim_start.elapsed();
1355
1356            // Update graphics from terminal (pass scroll_offset for view adjustment)
1357            // Include both current screen graphics and scrollback graphics
1358            // Use get_graphics_with_animations() to get current animation frames
1359            let graphics_start = std::time::Instant::now();
1360            if let Some(tab) = self.tab_manager.active_tab() {
1361                let terminal = tab.terminal.blocking_lock();
1362                let mut graphics = terminal.get_graphics_with_animations();
1363                let scrollback_len = terminal.scrollback_len();
1364
1365                // Always include scrollback graphics (renderer will calculate visibility)
1366                let scrollback_graphics = terminal.get_scrollback_graphics();
1367                let scrollback_count = scrollback_graphics.len();
1368                graphics.extend(scrollback_graphics);
1369
1370                debug_info!(
1371                    "APP",
1372                    "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
1373                    graphics.len(),
1374                    scrollback_count,
1375                    scroll_offset,
1376                    scrollback_len
1377                );
1378                if let Err(e) = renderer.update_graphics(
1379                    &graphics,
1380                    scroll_offset,
1381                    scrollback_len,
1382                    visible_lines,
1383                ) {
1384                    log::error!("Failed to update graphics: {}", e);
1385                }
1386            }
1387            debug_graphics_time = graphics_start.elapsed();
1388
1389            // Calculate visual bell flash intensity (0.0 = no flash, 1.0 = full flash)
1390            let visual_bell_flash = self
1391                .tab_manager
1392                .active_tab()
1393                .and_then(|t| t.bell.visual_flash);
1394            let visual_bell_intensity = if let Some(flash_start) = visual_bell_flash {
1395                const FLASH_DURATION_MS: u128 = 150;
1396                let elapsed = flash_start.elapsed().as_millis();
1397                if elapsed < FLASH_DURATION_MS {
1398                    // Request continuous redraws while flash is active
1399                    if let Some(window) = &self.window {
1400                        window.request_redraw();
1401                    }
1402                    // Fade out: start at 0.3 intensity, fade to 0
1403                    0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
1404                } else {
1405                    // Flash complete - clear it
1406                    if let Some(tab) = self.tab_manager.active_tab_mut() {
1407                        tab.bell.visual_flash = None;
1408                    }
1409                    0.0
1410                }
1411            } else {
1412                0.0
1413            };
1414
1415            // Update renderer with visual bell intensity
1416            renderer.set_visual_bell_intensity(visual_bell_intensity);
1417
1418            // Prepare egui output if settings UI is visible
1419            let egui_start = std::time::Instant::now();
1420
1421            // Capture values for FPS overlay before closure
1422            let show_fps = self.debug.show_fps_overlay;
1423            let fps_value = self.debug.fps_value;
1424            let frame_time_ms = if !self.debug.frame_times.is_empty() {
1425                let avg = self.debug.frame_times.iter().sum::<std::time::Duration>()
1426                    / self.debug.frame_times.len() as u32;
1427                avg.as_secs_f64() * 1000.0
1428            } else {
1429                0.0
1430            };
1431
1432            // Track config changes from settings UI (to be applied after egui block)
1433            #[allow(clippy::type_complexity)]
1434            let mut pending_config_update: Option<(
1435                Option<crate::config::Config>,
1436                Option<crate::config::Config>,
1437                Option<ShaderEditorResult>,
1438                Option<CursorShaderEditorResult>,
1439            )> = None;
1440
1441            let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
1442                (&self.egui_ctx, &mut self.egui_state)
1443            {
1444                let raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
1445
1446                let egui_output = egui_ctx.run(raw_input, |ctx| {
1447                    // Show FPS overlay if enabled (top-right corner)
1448                    if show_fps {
1449                        egui::Area::new(egui::Id::new("fps_overlay"))
1450                            .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
1451                            .order(egui::Order::Foreground)
1452                            .show(ctx, |ui| {
1453                                egui::Frame::NONE
1454                                    .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
1455                                    .inner_margin(egui::Margin::same(8))
1456                                    .corner_radius(4.0)
1457                                    .show(ui, |ui| {
1458                                        ui.style_mut().visuals.override_text_color =
1459                                            Some(egui::Color32::from_rgb(0, 255, 0));
1460                                        ui.label(
1461                                            egui::RichText::new(format!(
1462                                                "FPS: {:.1}\nFrame: {:.2}ms",
1463                                                fps_value, frame_time_ms
1464                                            ))
1465                                            .monospace()
1466                                            .size(14.0),
1467                                        );
1468                                    });
1469                            });
1470                    }
1471
1472                    // Render tab bar if visible (action handled after closure)
1473                    pending_tab_action =
1474                        self.tab_bar_ui.render(ctx, &self.tab_manager, &self.config);
1475
1476                    // Show settings UI and store results for later processing
1477                    let settings_result = self.settings_ui.show(ctx);
1478                    pending_config_update = Some(settings_result);
1479
1480                    // Show help UI
1481                    self.help_ui.show(ctx);
1482
1483                    // Show clipboard history UI and collect action
1484                    pending_clipboard_action = self.clipboard_history_ui.show(ctx);
1485                });
1486
1487                // Handle egui platform output (clipboard, cursor changes, etc.)
1488                // This enables cut/copy/paste in egui text editors
1489                egui_state.handle_platform_output(
1490                    self.window.as_ref().unwrap(),
1491                    egui_output.platform_output.clone(),
1492                );
1493
1494                Some((egui_output, egui_ctx))
1495            } else {
1496                None
1497            };
1498
1499            // Process settings changes after egui block (to avoid borrow conflicts)
1500            if let Some((
1501                config_to_save,
1502                config_for_live_update,
1503                shader_apply,
1504                cursor_shader_apply,
1505            )) = pending_config_update
1506            {
1507                // Handle background shader apply request first
1508                if let Some(shader_result) = shader_apply {
1509                    log::info!(
1510                        "Applying background shader from editor ({} bytes)",
1511                        shader_result.source.len()
1512                    );
1513                    match renderer.reload_shader_from_source(&shader_result.source) {
1514                        Ok(()) => {
1515                            log::info!("Background shader applied successfully from editor");
1516                            self.settings_ui.clear_shader_error();
1517                        }
1518                        Err(e) => {
1519                            let error_msg = format!("{:#}", e);
1520                            log::error!("Background shader compilation failed: {}", error_msg);
1521                            self.settings_ui.set_shader_error(Some(error_msg));
1522                        }
1523                    }
1524                }
1525
1526                // Handle cursor shader apply request
1527                if let Some(cursor_shader_result) = cursor_shader_apply {
1528                    log::info!(
1529                        "Applying cursor shader from editor ({} bytes)",
1530                        cursor_shader_result.source.len()
1531                    );
1532                    match renderer.reload_cursor_shader_from_source(&cursor_shader_result.source) {
1533                        Ok(()) => {
1534                            log::info!("Cursor shader applied successfully from editor");
1535                            self.settings_ui.clear_cursor_shader_error();
1536                        }
1537                        Err(e) => {
1538                            let error_msg = format!("{:#}", e);
1539                            log::error!("Cursor shader compilation failed: {}", error_msg);
1540                            self.settings_ui.set_cursor_shader_error(Some(error_msg));
1541                        }
1542                    }
1543                }
1544                // Apply live updates immediately (for visual feedback)
1545                if let Some(live_config) = config_for_live_update {
1546                    let theme_changed = live_config.theme != self.config.theme;
1547                    let shader_animation_changed =
1548                        live_config.custom_shader_animation != self.config.custom_shader_animation;
1549                    let shader_enabled_changed =
1550                        live_config.custom_shader_enabled != self.config.custom_shader_enabled;
1551                    let shader_path_changed =
1552                        live_config.custom_shader != self.config.custom_shader;
1553                    let shader_speed_changed = (live_config.custom_shader_animation_speed
1554                        - self.config.custom_shader_animation_speed)
1555                        .abs()
1556                        > f32::EPSILON;
1557                    let shader_full_content_changed = live_config.custom_shader_full_content
1558                        != self.config.custom_shader_full_content;
1559                    let shader_text_opacity_changed = (live_config.custom_shader_text_opacity
1560                        - self.config.custom_shader_text_opacity)
1561                        .abs()
1562                        > f32::EPSILON;
1563                    let shader_brightness_changed = (live_config.custom_shader_brightness
1564                        - self.config.custom_shader_brightness)
1565                        .abs()
1566                        > f32::EPSILON;
1567                    let cursor_shader_config_changed = live_config.cursor_shader_color
1568                        != self.config.cursor_shader_color
1569                        || (live_config.cursor_shader_trail_duration
1570                            - self.config.cursor_shader_trail_duration)
1571                            .abs()
1572                            > f32::EPSILON
1573                        || (live_config.cursor_shader_glow_radius
1574                            - self.config.cursor_shader_glow_radius)
1575                            .abs()
1576                            > f32::EPSILON
1577                        || (live_config.cursor_shader_glow_intensity
1578                            - self.config.cursor_shader_glow_intensity)
1579                            .abs()
1580                            > f32::EPSILON;
1581                    let cursor_shader_path_changed =
1582                        live_config.cursor_shader != self.config.cursor_shader;
1583                    let cursor_shader_enabled_changed =
1584                        live_config.cursor_shader_enabled != self.config.cursor_shader_enabled;
1585                    let cursor_shader_animation_changed =
1586                        live_config.cursor_shader_animation != self.config.cursor_shader_animation;
1587                    let cursor_shader_speed_changed = (live_config.cursor_shader_animation_speed
1588                        - self.config.cursor_shader_animation_speed)
1589                        .abs()
1590                        > f32::EPSILON;
1591                    let cursor_shader_hides_cursor_changed = live_config.cursor_shader_hides_cursor
1592                        != self.config.cursor_shader_hides_cursor;
1593                    let _scrollbar_position_changed =
1594                        live_config.scrollbar_position != self.config.scrollbar_position;
1595                    let window_title_changed = live_config.window_title != self.config.window_title;
1596                    let window_decorations_changed =
1597                        live_config.window_decorations != self.config.window_decorations;
1598                    let max_fps_changed = live_config.max_fps != self.config.max_fps;
1599                    let cursor_style_changed = live_config.cursor_style != self.config.cursor_style;
1600                    let cursor_color_changed = live_config.cursor_color != self.config.cursor_color;
1601                    let bg_enabled_changed = live_config.background_image_enabled
1602                        != self.config.background_image_enabled;
1603                    let bg_path_changed =
1604                        live_config.background_image != self.config.background_image;
1605                    let bg_mode_changed =
1606                        live_config.background_image_mode != self.config.background_image_mode;
1607                    let bg_opacity_changed = (live_config.background_image_opacity
1608                        - self.config.background_image_opacity)
1609                        .abs()
1610                        > f32::EPSILON;
1611                    let font_changed = live_config.font_family != self.config.font_family
1612                        || live_config.font_family_bold != self.config.font_family_bold
1613                        || live_config.font_family_italic != self.config.font_family_italic
1614                        || live_config.font_family_bold_italic
1615                            != self.config.font_family_bold_italic
1616                        || (live_config.font_size - self.config.font_size).abs() > f32::EPSILON
1617                        || (live_config.line_spacing - self.config.line_spacing).abs()
1618                            > f32::EPSILON
1619                        || (live_config.char_spacing - self.config.char_spacing).abs()
1620                            > f32::EPSILON;
1621                    let padding_changed = (live_config.window_padding - self.config.window_padding)
1622                        .abs()
1623                        > f32::EPSILON;
1624                    log::info!(
1625                        "Applying live config update - opacity: {}{}{}",
1626                        live_config.window_opacity,
1627                        if theme_changed {
1628                            " (theme changed)"
1629                        } else {
1630                            ""
1631                        },
1632                        if font_changed { " (font changed)" } else { "" }
1633                    );
1634                    self.config = live_config;
1635                    if let Some(tab) = self.tab_manager.active_tab_mut() {
1636                        tab.scroll_state.last_activity = std::time::Instant::now();
1637                    }
1638
1639                    // Apply settings that can be updated in real-time
1640                    if let Some(window) = &self.window {
1641                        // Update window level (always on top)
1642                        window.set_window_level(if self.config.window_always_on_top {
1643                            winit::window::WindowLevel::AlwaysOnTop
1644                        } else {
1645                            winit::window::WindowLevel::Normal
1646                        });
1647
1648                        // Update window title
1649                        if window_title_changed {
1650                            window.set_title(&self.config.window_title);
1651                            log::info!("Updated window title to: {}", self.config.window_title);
1652                        }
1653
1654                        // Update window decorations
1655                        if window_decorations_changed {
1656                            window.set_decorations(self.config.window_decorations);
1657                            log::info!(
1658                                "Updated window decorations: {}",
1659                                self.config.window_decorations
1660                            );
1661                        }
1662
1663                        // Request redraw to apply visual changes
1664                        window.request_redraw();
1665                    }
1666
1667                    // Update max_fps (restart refresh timer with new interval for all tabs)
1668                    if max_fps_changed {
1669                        // Update all tabs' refresh tasks
1670                        if let Some(window) = &self.window {
1671                            for tab in self.tab_manager.tabs_mut() {
1672                                tab.stop_refresh_task();
1673                                tab.start_refresh_task(
1674                                    Arc::clone(&self.runtime),
1675                                    Arc::clone(window),
1676                                    self.config.max_fps,
1677                                );
1678                            }
1679                            log::info!("Updated max_fps to {} for all tabs", self.config.max_fps);
1680                        }
1681                    }
1682
1683                    // Update renderer with real-time settings
1684                    renderer.update_opacity(self.config.window_opacity);
1685                    renderer.update_scrollbar_appearance(
1686                        self.config.scrollbar_width,
1687                        self.config.scrollbar_thumb_color,
1688                        self.config.scrollbar_track_color,
1689                    );
1690                    // Scrollbar position is now fixed to right; ignore config changes
1691
1692                    if cursor_style_changed {
1693                        // Set cursor style directly on the terminal (no need to send DECSCUSR to PTY)
1694                        // This updates the terminal's internal cursor state without involving the shell
1695                        if let Some(tab) = self.tab_manager.active_tab()
1696                            && let Ok(term_mgr) = tab.terminal.try_lock()
1697                        {
1698                            // Get the underlying Terminal from TerminalManager
1699                            let terminal = term_mgr.terminal();
1700                            if let Some(mut term) = terminal.try_lock() {
1701                                // Convert config cursor style to terminal cursor style
1702                                use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
1703                                let term_style = match self.config.cursor_style {
1704                                    crate::config::CursorStyle::Block => {
1705                                        TermCursorStyle::SteadyBlock
1706                                    }
1707                                    crate::config::CursorStyle::Underline => {
1708                                        TermCursorStyle::SteadyUnderline
1709                                    }
1710                                    crate::config::CursorStyle::Beam => TermCursorStyle::SteadyBar,
1711                                };
1712                                term.set_cursor_style(term_style);
1713                            }
1714                        }
1715
1716                        // Force cell regen to reflect cursor style change
1717                        if let Some(tab) = self.tab_manager.active_tab_mut() {
1718                            tab.cache.cells = None;
1719                            tab.cache.cursor_pos = None;
1720                        }
1721                        if let Some(window) = &self.window {
1722                            window.request_redraw();
1723                        }
1724                    }
1725
1726                    // Update cursor color
1727                    if cursor_color_changed {
1728                        renderer.update_cursor_color(self.config.cursor_color);
1729                        // Force cell regen to reflect cursor color change
1730                        if let Some(tab) = self.tab_manager.active_tab_mut() {
1731                            tab.cache.cells = None;
1732                            tab.cache.cursor_pos = None;
1733                        }
1734                        if let Some(window) = &self.window {
1735                            window.request_redraw();
1736                        }
1737                    }
1738
1739                    if self.config.background_image_enabled {
1740                        renderer
1741                            .update_background_image_opacity(self.config.background_image_opacity);
1742                    }
1743
1744                    if bg_enabled_changed
1745                        || bg_path_changed
1746                        || bg_mode_changed
1747                        || bg_opacity_changed
1748                    {
1749                        renderer.set_background_image_enabled(
1750                            self.config.background_image_enabled,
1751                            self.config.background_image.as_deref(),
1752                            self.config.background_image_mode,
1753                            self.config.background_image_opacity,
1754                        );
1755                    }
1756
1757                    if shader_animation_changed
1758                        || shader_enabled_changed
1759                        || shader_path_changed
1760                        || shader_speed_changed
1761                        || shader_full_content_changed
1762                        || shader_text_opacity_changed
1763                        || shader_brightness_changed
1764                    {
1765                        match renderer.set_custom_shader_enabled(
1766                            self.config.custom_shader_enabled,
1767                            self.config.custom_shader.as_deref(),
1768                            self.config.window_opacity,
1769                            self.config.custom_shader_text_opacity,
1770                            self.config.custom_shader_animation,
1771                            self.config.custom_shader_animation_speed,
1772                            self.config.custom_shader_full_content,
1773                            self.config.custom_shader_brightness,
1774                            &self.config.shader_channel_paths(),
1775                        ) {
1776                            Ok(()) => {
1777                                self.settings_ui.clear_shader_error();
1778                            }
1779                            Err(error_msg) => {
1780                                log::error!("Shader compilation failed: {}", error_msg);
1781                                self.settings_ui.set_shader_error(Some(error_msg));
1782                            }
1783                        }
1784                    }
1785
1786                    // Update cursor shader configuration
1787                    if cursor_shader_config_changed {
1788                        renderer.update_cursor_shader_config(
1789                            self.config.cursor_shader_color,
1790                            self.config.cursor_shader_trail_duration,
1791                            self.config.cursor_shader_glow_radius,
1792                            self.config.cursor_shader_glow_intensity,
1793                        );
1794                    }
1795
1796                    // Handle cursor shader path/enabled/animation changes
1797                    if cursor_shader_path_changed
1798                        || cursor_shader_enabled_changed
1799                        || cursor_shader_animation_changed
1800                        || cursor_shader_speed_changed
1801                    {
1802                        match renderer.set_cursor_shader_enabled(
1803                            self.config.cursor_shader_enabled,
1804                            self.config.cursor_shader.as_deref(),
1805                            self.config.window_opacity,
1806                            self.config.cursor_shader_animation,
1807                            self.config.cursor_shader_animation_speed,
1808                        ) {
1809                            Ok(()) => {
1810                                self.settings_ui.clear_cursor_shader_error();
1811                            }
1812                            Err(error_msg) => {
1813                                log::error!("Cursor shader compilation failed: {}", error_msg);
1814                                self.settings_ui.set_cursor_shader_error(Some(error_msg));
1815                            }
1816                        }
1817                    }
1818
1819                    // Update cursor hidden state when shader enabled or hides_cursor setting changes
1820                    if cursor_shader_enabled_changed || cursor_shader_hides_cursor_changed {
1821                        renderer.set_cursor_hidden_for_shader(
1822                            self.config.cursor_shader_enabled
1823                                && self.config.cursor_shader_hides_cursor,
1824                        );
1825                    }
1826
1827                    // Apply theme changes immediately to the terminal
1828                    if theme_changed {
1829                        if let Some(tab) = self.tab_manager.active_tab()
1830                            && let Ok(mut term) = tab.terminal.try_lock()
1831                        {
1832                            term.set_theme(self.config.load_theme());
1833                            log::info!("Applied live theme change: {}", self.config.theme);
1834                        }
1835                        // Force redraw so new theme colors show up right away
1836                        if let Some(tab) = self.tab_manager.active_tab_mut() {
1837                            tab.cache.cells = None;
1838                        }
1839                        if let Some(window) = &self.window {
1840                            window.request_redraw();
1841                        }
1842                    }
1843
1844                    if font_changed {
1845                        // Rebuild renderer on next frame to apply font/spacing changes without restart
1846                        self.pending_font_rebuild = true;
1847                        log::info!("Queued renderer rebuild for font change");
1848                    }
1849
1850                    // Update window padding dynamically (no rebuild needed)
1851                    if padding_changed {
1852                        if let Some((cols, rows)) =
1853                            renderer.update_window_padding(self.config.window_padding)
1854                        {
1855                            // Grid size changed - resize all tabs' terminals to match
1856                            let cell_width = renderer.cell_width();
1857                            let cell_height = renderer.cell_height();
1858                            let width_px = (cols as f32 * cell_width) as usize;
1859                            let height_px = (rows as f32 * cell_height) as usize;
1860
1861                            for tab in self.tab_manager.tabs_mut() {
1862                                if let Ok(mut term) = tab.terminal.try_lock() {
1863                                    let _ =
1864                                        term.resize_with_pixels(cols, rows, width_px, height_px);
1865                                }
1866                            }
1867                            log::info!(
1868                                "Resized terminals to {}x{} due to padding change",
1869                                cols,
1870                                rows
1871                            );
1872                        }
1873                        log::info!("Updated window padding to {}", self.config.window_padding);
1874                    }
1875
1876                    // Invalidate cell cache to force regeneration with new opacity
1877                    if let Some(tab) = self.tab_manager.active_tab_mut() {
1878                        tab.cache.cells = None;
1879                        // Track last applied opacity
1880                        tab.cache.applied_opacity = self.config.window_opacity;
1881                    }
1882
1883                    // Request multiple redraws to ensure changes are visible
1884                    if let Some(window) = &self.window {
1885                        window.request_redraw();
1886                    }
1887                }
1888
1889                // Persist to disk if save was clicked
1890                if let Some(new_config) = config_to_save {
1891                    if let Err(e) = new_config.save() {
1892                        log::error!("Failed to save config: {}", e);
1893                    } else {
1894                        log::info!("Configuration saved successfully");
1895                        log::info!(
1896                            "  Bell settings: sound={}, visual={}, desktop={}",
1897                            new_config.notification_bell_sound,
1898                            new_config.notification_bell_visual,
1899                            new_config.notification_bell_desktop
1900                        );
1901                        // Update settings_ui with saved config
1902                        self.settings_ui.update_config(new_config);
1903                    }
1904                }
1905            }
1906            let debug_egui_time = egui_start.elapsed();
1907
1908            // Calculate FPS and timing stats
1909            let avg_frame_time = if !self.debug.frame_times.is_empty() {
1910                self.debug.frame_times.iter().sum::<std::time::Duration>()
1911                    / self.debug.frame_times.len() as u32
1912            } else {
1913                std::time::Duration::ZERO
1914            };
1915            let fps = if avg_frame_time.as_secs_f64() > 0.0 {
1916                1.0 / avg_frame_time.as_secs_f64()
1917            } else {
1918                0.0
1919            };
1920
1921            // Update FPS value for overlay display
1922            self.debug.fps_value = fps;
1923
1924            // Log debug info every 60 frames (about once per second at 60 FPS)
1925            if self.debug.frame_times.len() >= 60 {
1926                let (cache_gen, cache_has_cells) = self
1927                    .tab_manager
1928                    .active_tab()
1929                    .map(|t| (t.cache.generation, t.cache.cells.is_some()))
1930                    .unwrap_or((0, false));
1931                log::info!(
1932                    "PERF: FPS={:.1} Frame={:.2}ms CellGen={:.2}ms({}) URLDetect={:.2}ms Anim={:.2}ms Graphics={:.2}ms egui={:.2}ms UpdateCells={:.2}ms ActualRender={:.2}ms Total={:.2}ms Cells={} Gen={} Cache={}",
1933                    fps,
1934                    avg_frame_time.as_secs_f64() * 1000.0,
1935                    self.debug.cell_gen_time.as_secs_f64() * 1000.0,
1936                    if self.debug.cache_hit { "HIT" } else { "MISS" },
1937                    debug_url_detect_time.as_secs_f64() * 1000.0,
1938                    debug_anim_time.as_secs_f64() * 1000.0,
1939                    debug_graphics_time.as_secs_f64() * 1000.0,
1940                    debug_egui_time.as_secs_f64() * 1000.0,
1941                    debug_update_cells_time.as_secs_f64() * 1000.0,
1942                    debug_actual_render_time.as_secs_f64() * 1000.0,
1943                    self.debug.render_time.as_secs_f64() * 1000.0,
1944                    cells.len(),
1945                    cache_gen,
1946                    if cache_has_cells { "YES" } else { "NO" }
1947                );
1948            }
1949
1950            // Render (with dirty tracking optimization)
1951            let actual_render_start = std::time::Instant::now();
1952            match renderer.render(egui_data, self.settings_ui.visible, show_scrollbar) {
1953                Ok(rendered) => {
1954                    if !rendered {
1955                        log::trace!("Skipped rendering - no changes");
1956                    }
1957                }
1958                Err(e) => {
1959                    // Check if this is a wgpu surface error that requires reconfiguration
1960                    // This commonly happens when dragging windows between displays
1961                    if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
1962                        match surface_error {
1963                            SurfaceError::Outdated | SurfaceError::Lost => {
1964                                log::warn!(
1965                                    "Surface error detected ({:?}), reconfiguring...",
1966                                    surface_error
1967                                );
1968                                self.force_surface_reconfigure();
1969                            }
1970                            SurfaceError::Timeout => {
1971                                log::warn!("Surface timeout, will retry next frame");
1972                                if let Some(window) = &self.window {
1973                                    window.request_redraw();
1974                                }
1975                            }
1976                            SurfaceError::OutOfMemory => {
1977                                log::error!("Surface out of memory: {:?}", surface_error);
1978                            }
1979                            _ => {
1980                                log::error!("Surface error: {:?}", surface_error);
1981                            }
1982                        }
1983                    } else {
1984                        log::error!("Render error: {}", e);
1985                    }
1986                }
1987            }
1988            debug_actual_render_time = actual_render_start.elapsed();
1989            let _ = debug_actual_render_time;
1990
1991            self.debug.render_time = render_start.elapsed();
1992        }
1993
1994        // Handle tab bar actions collected during egui rendering
1995        // (done here to avoid borrow conflicts with renderer)
1996        match pending_tab_action {
1997            TabBarAction::SwitchTo(id) => {
1998                self.tab_manager.switch_to(id);
1999                // Clear renderer cells and invalidate cache to ensure clean switch
2000                if let Some(renderer) = &mut self.renderer {
2001                    renderer.clear_all_cells();
2002                }
2003                if let Some(tab) = self.tab_manager.active_tab_mut() {
2004                    tab.cache.cells = None;
2005                }
2006                if let Some(window) = &self.window {
2007                    window.request_redraw();
2008                }
2009            }
2010            TabBarAction::Close(id) => {
2011                let was_last = self.tab_manager.close_tab(id);
2012                if was_last {
2013                    // Last tab closed - close window
2014                    self.is_shutting_down = true;
2015                }
2016                if let Some(window) = &self.window {
2017                    window.request_redraw();
2018                }
2019            }
2020            TabBarAction::NewTab => {
2021                self.new_tab();
2022                if let Some(window) = &self.window {
2023                    window.request_redraw();
2024                }
2025            }
2026            TabBarAction::None | TabBarAction::Reorder(_, _) => {}
2027        }
2028
2029        // Handle clipboard actions collected during egui rendering
2030        // (done here to avoid borrow conflicts with renderer)
2031        match pending_clipboard_action {
2032            ClipboardHistoryAction::Paste(content) => {
2033                self.paste_text(&content);
2034            }
2035            ClipboardHistoryAction::ClearAll => {
2036                if let Some(tab) = self.tab_manager.active_tab()
2037                    && let Ok(term) = tab.terminal.try_lock()
2038                {
2039                    term.clear_all_clipboard_history();
2040                    log::info!("Cleared all clipboard history");
2041                }
2042                self.clipboard_history_ui.update_entries(Vec::new());
2043            }
2044            ClipboardHistoryAction::ClearSlot(slot) => {
2045                if let Some(tab) = self.tab_manager.active_tab()
2046                    && let Ok(term) = tab.terminal.try_lock()
2047                {
2048                    term.clear_clipboard_history(slot);
2049                    log::info!("Cleared clipboard history for slot {:?}", slot);
2050                }
2051            }
2052            ClipboardHistoryAction::None => {}
2053        }
2054
2055        let absolute_total = absolute_start.elapsed();
2056        if absolute_total.as_millis() > 10 {
2057            log::debug!(
2058                "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
2059                absolute_total.as_secs_f64() * 1000.0
2060            );
2061        }
2062    }
2063}
2064
2065impl Drop for WindowState {
2066    fn drop(&mut self) {
2067        log::info!("Shutting down window");
2068
2069        // Set shutdown flag
2070        self.is_shutting_down = true;
2071
2072        // Clean up all tabs
2073        let tab_count = self.tab_manager.tab_count();
2074        log::info!("Cleaning up {} tabs", tab_count);
2075
2076        // Stop all refresh tasks first
2077        for tab in self.tab_manager.tabs_mut() {
2078            tab.stop_refresh_task();
2079        }
2080        log::info!("All refresh tasks aborted");
2081
2082        // Give abort time to take effect and any pending operations to complete
2083        std::thread::sleep(std::time::Duration::from_millis(100));
2084
2085        // Kill all PTY processes
2086        for tab in self.tab_manager.tabs_mut() {
2087            if let Ok(mut term) = tab.terminal.try_lock() {
2088                if term.is_running() {
2089                    log::info!("Killing PTY process for tab {}", tab.id);
2090                    match term.kill() {
2091                        Ok(()) => {
2092                            log::info!("PTY process killed successfully for tab {}", tab.id);
2093                        }
2094                        Err(e) => {
2095                            log::warn!("Failed to kill PTY process for tab {}: {:?}", tab.id, e);
2096                        }
2097                    }
2098                } else {
2099                    log::info!("PTY process already stopped for tab {}", tab.id);
2100                }
2101            } else {
2102                log::warn!(
2103                    "Could not acquire terminal lock to kill PTY for tab {}",
2104                    tab.id
2105                );
2106            }
2107        }
2108
2109        // Give the PTY time to clean up after kill signal
2110        std::thread::sleep(std::time::Duration::from_millis(100));
2111
2112        log::info!("Window shutdown complete");
2113    }
2114}