par_term/
app.rs

1use crate::cell_renderer::Cell;
2use crate::clipboard_history_ui::{ClipboardHistoryAction, ClipboardHistoryUI};
3use crate::config::Config;
4use crate::help_ui::HelpUI;
5use crate::input::InputHandler;
6use crate::renderer::Renderer;
7use crate::scroll_state::ScrollState;
8use crate::selection::{Selection, SelectionMode};
9use crate::settings_ui::{CursorShaderEditorResult, SettingsUI, ShaderEditorResult};
10use crate::terminal::{ClipboardSlot, TerminalManager};
11use crate::url_detection;
12use anyhow::Result;
13use std::sync::Arc;
14use tokio::runtime::Runtime;
15use tokio::sync::Mutex;
16use tokio::task::JoinHandle;
17use wgpu::SurfaceError;
18use winit::application::ApplicationHandler;
19use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
20use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
21use winit::window::{Window, WindowId};
22
23/// Main application state
24pub struct App {
25    config: Config,
26    runtime: Arc<Runtime>,
27}
28
29impl App {
30    /// Create a new application
31    pub fn new(runtime: Arc<Runtime>) -> Result<Self> {
32        let config = Config::load()?;
33        Ok(Self { config, runtime })
34    }
35
36    /// Run the application
37    pub fn run(self) -> Result<()> {
38        let event_loop = EventLoop::new()?;
39        // Use Poll instead of Wait to enable continuous rendering at 60 FPS
40        // Combined with PresentMode::Immediate for maximum performance
41        event_loop.set_control_flow(ControlFlow::Wait);
42
43        let mut app_state = AppState::new(self.config, self.runtime);
44
45        event_loop.run_app(&mut app_state)?;
46
47        Ok(())
48    }
49}
50
51/// Application state that handles events
52struct AppState {
53    config: Config,
54    window: Option<Arc<Window>>,
55    renderer: Option<Renderer>,
56    terminal: Option<Arc<Mutex<TerminalManager>>>,
57    input_handler: InputHandler,
58    refresh_task: Option<JoinHandle<()>>,
59    runtime: Arc<Runtime>,
60    scroll_state: ScrollState,
61
62    selection: Option<Selection>, // Current text selection
63    is_selecting: bool,           // Whether currently dragging to select
64
65    mouse_position: (f64, f64),   // Current mouse position in pixels
66    cached_scrollback_len: usize, // Last known scrollback length
67    mouse_button_pressed: bool, // Whether any mouse button is currently pressed (for motion tracking)
68    last_click_time: Option<std::time::Instant>, // Time of last mouse click
69    click_count: u32,           // Number of sequential clicks (1 = single, 2 = double, 3 = triple)
70    click_position: Option<(usize, usize)>, // Position of last click in cell coordinates
71    detected_urls: Vec<url_detection::DetectedUrl>, // URLs detected in visible terminal area
72    hovered_url: Option<String>, // URL currently under mouse cursor
73    cursor_opacity: f32, // Cursor opacity for smooth fade animation (0.0 = invisible, 1.0 = fully visible)
74    last_cursor_blink: Option<std::time::Instant>, // Time of last cursor blink toggle
75    last_key_press: Option<std::time::Instant>, // Time of last key press (to reset cursor blink)
76    is_fullscreen: bool, // Whether window is currently in fullscreen mode
77    audio_bell: Option<crate::audio_bell::AudioBell>, // Audio bell for terminal bell sounds
78    last_bell_count: u64, // Last bell event count from terminal
79    visual_bell_flash: Option<std::time::Instant>, // When visual bell flash started (None = not flashing)
80    egui_ctx: Option<egui::Context>,               // egui context for GUI rendering
81    egui_state: Option<egui_winit::State>,         // egui-winit state for event handling
82    settings_ui: SettingsUI,                       // Settings UI manager
83    help_ui: HelpUI,                               // Help UI manager
84    clipboard_history_ui: ClipboardHistoryUI,      // Clipboard history UI manager
85    is_recording: bool,                            // Whether terminal session recording is active
86    #[allow(dead_code)] // Used in recording feature but clippy doesn't detect it
87    recording_start_time: Option<std::time::Instant>, // When recording started
88    is_shutting_down: bool,                        // Flag to indicate shutdown is in progress
89    cached_cells: Option<Vec<Cell>>,               // Cached cells from last render (dirty tracking)
90    last_generation: u64, // Last terminal generation number (for dirty tracking)
91    last_scroll_offset: usize, // Last scroll offset (for cache invalidation)
92    last_cursor_pos: Option<(usize, usize)>, // Last cursor position (for cache invalidation)
93    last_selection: Option<Selection>, // Last selection state (for cache invalidation)
94    last_applied_opacity: f32, // Last opacity value sent to renderer
95
96    // Smart redraw tracking (event-driven rendering)
97    needs_redraw: bool, // Whether we need to render next frame
98    cursor_blink_timer: Option<std::time::Instant>, // When to blink cursor next
99    // Debug timing info
100    debug_frame_times: Vec<std::time::Duration>, // Last 60 frame times for FPS calculation
101    debug_cell_gen_time: std::time::Duration,    // Time spent generating cells last frame
102    debug_render_time: std::time::Duration,      // Time spent rendering last frame
103    debug_cache_hit: bool,                       // Whether last frame used cached cells
104    debug_last_frame_start: Option<std::time::Instant>, // Start time of last frame
105    // FPS overlay
106    show_fps_overlay: bool,     // Whether to show FPS overlay (toggle with F3)
107    fps_value: f64,             // Current FPS value for overlay display
108    pending_font_rebuild: bool, // Whether we need to rebuild renderer after font-related changes
109    cached_terminal_title: String, // Last known terminal title (for change detection)
110}
111
112impl AppState {
113    /// Extract a substring based on character columns to avoid UTF-8 slicing panics
114    fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
115        let mut extracted = String::new();
116        let end_bound = end_col.unwrap_or(usize::MAX);
117
118        if start_col > end_bound {
119            return extracted;
120        }
121
122        for (idx, ch) in line.chars().enumerate() {
123            if idx > end_bound {
124                break;
125            }
126
127            if idx >= start_col {
128                extracted.push(ch);
129            }
130        }
131
132        extracted
133    }
134
135    fn new(config: Config, runtime: Arc<Runtime>) -> Self {
136        let initial_opacity = config.window_opacity;
137        let settings_ui = SettingsUI::new(config.clone());
138
139        Self {
140            config,
141            window: None,
142            renderer: None,
143            terminal: None,
144            input_handler: InputHandler::new(),
145            refresh_task: None,
146            runtime,
147            scroll_state: ScrollState::new(),
148
149            selection: None,
150            is_selecting: false,
151
152            mouse_position: (0.0, 0.0),
153            cached_scrollback_len: 0,
154            mouse_button_pressed: false,
155            last_click_time: None,
156            click_count: 0,
157            click_position: None,
158            detected_urls: Vec::new(),
159            hovered_url: None,
160            cursor_opacity: 1.0,
161            last_cursor_blink: None,
162            last_key_press: None,
163            is_fullscreen: false,
164            audio_bell: {
165                match crate::audio_bell::AudioBell::new() {
166                    Ok(bell) => {
167                        log::info!("Audio bell initialized successfully");
168                        Some(bell)
169                    }
170                    Err(e) => {
171                        log::warn!("Failed to initialize audio bell: {}", e);
172                        None
173                    }
174                }
175            },
176            last_bell_count: 0,
177            visual_bell_flash: None,
178            egui_ctx: None,
179            egui_state: None,
180            settings_ui,
181            help_ui: HelpUI::new(),
182            clipboard_history_ui: ClipboardHistoryUI::new(),
183            is_recording: false,
184            recording_start_time: None,
185            is_shutting_down: false,
186            cached_cells: None,
187            last_generation: 0,
188            last_scroll_offset: 0,
189            last_cursor_pos: None,
190            last_selection: None,
191            last_applied_opacity: initial_opacity,
192
193            needs_redraw: true,
194            cursor_blink_timer: None,
195            debug_frame_times: Vec::with_capacity(60),
196            debug_cell_gen_time: std::time::Duration::ZERO,
197            debug_render_time: std::time::Duration::ZERO,
198            debug_cache_hit: false,
199            debug_last_frame_start: None,
200            show_fps_overlay: false,
201            fps_value: 0.0,
202            pending_font_rebuild: false,
203            cached_terminal_title: String::new(),
204        }
205    }
206
207    /// Rebuild the renderer after font-related changes and resize the terminal accordingly
208    fn rebuild_renderer(&mut self) -> Result<()> {
209        let window = if let Some(w) = &self.window {
210            Arc::clone(w)
211        } else {
212            return Ok(()); // Nothing to rebuild yet
213        };
214
215        let theme = self.config.load_theme();
216        let font_family = if self.config.font_family.is_empty() {
217            None
218        } else {
219            Some(self.config.font_family.as_str())
220        };
221
222        let mut renderer = self.runtime.block_on(Renderer::new(
223            Arc::clone(&window),
224            font_family,
225            self.config.font_family_bold.as_deref(),
226            self.config.font_family_italic.as_deref(),
227            self.config.font_family_bold_italic.as_deref(),
228            &self.config.font_ranges,
229            self.config.font_size,
230            self.config.window_padding,
231            self.config.line_spacing,
232            self.config.char_spacing,
233            &self.config.scrollbar_position,
234            self.config.scrollbar_width,
235            self.config.scrollbar_thumb_color,
236            self.config.scrollbar_track_color,
237            self.config.enable_text_shaping,
238            self.config.enable_ligatures,
239            self.config.enable_kerning,
240            self.config.vsync_mode,
241            self.config.window_opacity,
242            theme.background.as_array(),
243            self.config.background_image.as_deref(),
244            self.config.background_image_enabled,
245            self.config.background_image_mode,
246            self.config.background_image_opacity,
247            self.config.custom_shader.as_deref(),
248            self.config.custom_shader_enabled,
249            self.config.custom_shader_animation,
250            self.config.custom_shader_animation_speed,
251            self.config.custom_shader_text_opacity,
252            self.config.custom_shader_full_content,
253            // Cursor shader settings
254            self.config.cursor_shader.as_deref(),
255            self.config.cursor_shader_enabled,
256            self.config.cursor_shader_animation,
257            self.config.cursor_shader_animation_speed,
258        ))?;
259
260        let (cols, rows) = renderer.grid_size();
261        let cell_width = renderer.cell_width();
262        let cell_height = renderer.cell_height();
263        let width_px = (cols as f32 * cell_width) as usize;
264        let height_px = (rows as f32 * cell_height) as usize;
265
266        if let Some(terminal) = &self.terminal
267            && let Ok(mut term) = terminal.try_lock()
268        {
269            let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
270            term.set_cell_dimensions(cell_width as u32, cell_height as u32);
271            term.set_theme(self.config.load_theme());
272        }
273
274        // Initialize cursor shader config
275        renderer.update_cursor_shader_config(
276            self.config.cursor_shader_color,
277            self.config.cursor_shader_trail_duration,
278            self.config.cursor_shader_glow_radius,
279            self.config.cursor_shader_glow_intensity,
280        );
281
282        // Initialize cursor color from config
283        renderer.update_cursor_color(self.config.cursor_color);
284
285        self.renderer = Some(renderer);
286        self.cached_cells = None;
287        self.needs_redraw = true;
288
289        // Reset egui GPU textures so the new renderer has a fresh atlas, but
290        // preserve window positions/collapse state by cloning the previous
291        // egui memory into the new context (otherwise the Settings window
292        // snaps to the top-left and all panels collapse after font changes).
293        let previous_memory = self
294            .egui_ctx
295            .as_ref()
296            .map(|ctx| ctx.memory(|mem| mem.clone()));
297
298        let scale_factor = window.scale_factor() as f32;
299        let egui_ctx = egui::Context::default();
300        if let Some(memory) = previous_memory {
301            egui_ctx.memory_mut(|mem| *mem = memory);
302        }
303        let egui_state = egui_winit::State::new(
304            egui_ctx.clone(),
305            egui::ViewportId::ROOT,
306            &window,
307            Some(scale_factor),
308            None,
309            None,
310        );
311        self.egui_ctx = Some(egui_ctx);
312        self.egui_state = Some(egui_state);
313
314        if let Some(window) = &self.window {
315            window.request_redraw();
316        }
317
318        Ok(())
319    }
320
321    async fn initialize_async(&mut self, window: Window) -> Result<()> {
322        // Enable IME (Input Method Editor) to receive all character events including Space
323        window.set_ime_allowed(true);
324        log::debug!("IME enabled for character input");
325
326        let window = Arc::new(window);
327
328        // Initialize egui context and state
329        let egui_ctx = egui::Context::default();
330        let egui_state = egui_winit::State::new(
331            egui_ctx.clone(),
332            egui::ViewportId::ROOT,
333            &window,
334            Some(window.scale_factor() as f32),
335            None,
336            None, // max_texture_side
337        );
338        self.egui_ctx = Some(egui_ctx);
339        self.egui_state = Some(egui_state);
340
341        // Create renderer with font family from config
342        let font_family = if self.config.font_family.is_empty() {
343            None
344        } else {
345            Some(self.config.font_family.as_str())
346        };
347        let theme = self.config.load_theme();
348        let mut renderer = Renderer::new(
349            Arc::clone(&window),
350            font_family,
351            self.config.font_family_bold.as_deref(),
352            self.config.font_family_italic.as_deref(),
353            self.config.font_family_bold_italic.as_deref(),
354            &self.config.font_ranges,
355            self.config.font_size,
356            self.config.window_padding,
357            self.config.line_spacing,
358            self.config.char_spacing,
359            &self.config.scrollbar_position,
360            self.config.scrollbar_width,
361            self.config.scrollbar_thumb_color,
362            self.config.scrollbar_track_color,
363            self.config.enable_text_shaping,
364            self.config.enable_ligatures,
365            self.config.enable_kerning,
366            self.config.vsync_mode,
367            self.config.window_opacity,
368            theme.background.as_array(),
369            self.config.background_image.as_deref(),
370            self.config.background_image_enabled,
371            self.config.background_image_mode,
372            self.config.background_image_opacity,
373            self.config.custom_shader.as_deref(),
374            self.config.custom_shader_enabled,
375            self.config.custom_shader_animation,
376            self.config.custom_shader_animation_speed,
377            self.config.custom_shader_text_opacity,
378            self.config.custom_shader_full_content,
379            // Cursor shader settings
380            self.config.cursor_shader.as_deref(),
381            self.config.cursor_shader_enabled,
382            self.config.cursor_shader_animation,
383            self.config.cursor_shader_animation_speed,
384        )
385        .await?;
386
387        // macOS: also update the NSWindow alpha so the OS compositor reflects live opacity
388        // macOS: Configure CAMetalLayer (transparency + performance)
389        // This MUST be done AFTER creating the wgpu surface/renderer
390        // so that the CAMetalLayer has been created by wgpu
391        #[cfg(target_os = "macos")]
392        {
393            if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
394                log::warn!("Failed to configure Metal layer: {}", e);
395                log::warn!(
396                    "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
397                );
398            }
399            // Set initial layer opacity to match config (content only, frame unaffected)
400            if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
401                log::warn!("Failed to set initial Metal layer opacity: {}", e);
402            }
403        }
404
405        // Create terminal with scrollback from config
406        let mut terminal = TerminalManager::new_with_scrollback(
407            self.config.cols,
408            self.config.rows,
409            self.config.scrollback_lines,
410        )?;
411
412        // Set theme from config
413        terminal.set_theme(self.config.load_theme());
414
415        // Apply clipboard history limits from config
416        terminal.set_max_clipboard_sync_events(self.config.clipboard_max_sync_events);
417        terminal.set_max_clipboard_event_bytes(self.config.clipboard_max_event_bytes);
418
419        // Ensure PTY dimensions match the renderer's computed grid
420        let (renderer_cols, renderer_rows) = renderer.grid_size();
421        log::info!(
422            "Initial terminal dimensions: {}x{}",
423            renderer_cols,
424            renderer_rows
425        );
426
427        // Calculate pixel dimensions and resize PTY with both character and pixel dimensions
428        // This is required for applications like kitty icat that query pixel dimensions via TIOCGWINSZ
429        let cell_width = renderer.cell_width();
430        let cell_height = renderer.cell_height();
431        let width_px = (renderer_cols as f32 * cell_width) as usize;
432        let height_px = (renderer_rows as f32 * cell_height) as usize;
433        terminal.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px)?;
434        log::info!(
435            "Initial terminal pixel dimensions: {}x{} px",
436            width_px,
437            height_px
438        );
439
440        // Spawn shell (custom or default) with optional working directory, args, and env vars
441        let working_dir = self.config.working_directory.as_deref();
442        let shell_env = self.config.shell_env.as_ref();
443
444        // Determine the shell command to use
445        let (shell_cmd, mut shell_args) = if let Some(ref custom) = self.config.custom_shell {
446            (custom.clone(), self.config.shell_args.clone())
447        } else {
448            #[cfg(target_os = "windows")]
449            {
450                ("powershell.exe".to_string(), None)
451            }
452            #[cfg(not(target_os = "windows"))]
453            {
454                (
455                    std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()),
456                    None,
457                )
458            }
459        };
460
461        // On Unix-like systems, spawn as login shell if configured (default: true)
462        // This ensures PATH is properly initialized from /etc/paths, ~/.zprofile, etc.
463        #[cfg(not(target_os = "windows"))]
464        if self.config.login_shell {
465            let args = shell_args.get_or_insert_with(Vec::new);
466            // Only add -l if not already present
467            if !args.iter().any(|a| a == "-l" || a == "--login") {
468                args.insert(0, "-l".to_string());
469            }
470        }
471
472        let shell_args_deref = shell_args.as_deref();
473        terminal.spawn_custom_shell_with_dir(
474            &shell_cmd,
475            shell_args_deref,
476            working_dir,
477            shell_env,
478        )?;
479
480        // Set cell dimensions on terminal for proper graphics scroll calculations
481        let cell_width = renderer.cell_width() as u32;
482        let cell_height = renderer.cell_height() as u32;
483        log::info!("Setting cell dimensions: {}x{}", cell_width, cell_height);
484        terminal.set_cell_dimensions(cell_width, cell_height);
485
486        // Initialize cursor shader config
487        renderer.update_cursor_shader_config(
488            self.config.cursor_shader_color,
489            self.config.cursor_shader_trail_duration,
490            self.config.cursor_shader_glow_radius,
491            self.config.cursor_shader_glow_intensity,
492        );
493
494        // Initialize cursor color from config
495        renderer.update_cursor_color(self.config.cursor_color);
496
497        self.window = Some(Arc::clone(&window));
498        self.renderer = Some(renderer);
499        self.terminal = Some(Arc::new(Mutex::new(terminal)));
500
501        // Start update polling task to check for terminal changes
502        let window_clone = Arc::clone(&window);
503        let terminal_clone = Arc::clone(self.terminal.as_ref().unwrap());
504        let max_fps = self.config.max_fps.max(1);
505        let refresh_interval_ms = 1000 / max_fps;
506
507        let handle = self.runtime.spawn(async move {
508            let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(
509                refresh_interval_ms as u64,
510            ));
511            // Track the last seen generation to detect changes
512            let mut last_gen = 0;
513
514            loop {
515                interval.tick().await;
516
517                // Check if terminal has updates (using generation counter if available, or has_updates flag)
518                // We use try_lock to avoid blocking the PTY thread
519                let should_redraw = if let Ok(term) = terminal_clone.try_lock() {
520                    let current_gen = term.update_generation();
521                    if current_gen > last_gen {
522                        last_gen = current_gen;
523                        true
524                    } else {
525                        // Also check for animations or other updates that might not bump generation
526                        term.has_updates()
527                    }
528                } else {
529                    // contention - retry next tick
530                    false
531                };
532
533                if should_redraw {
534                    window_clone.request_redraw();
535                }
536            }
537        });
538        self.refresh_task = Some(handle);
539
540        Ok(())
541    }
542
543    fn handle_key_event(&mut self, event: KeyEvent, event_loop: &ActiveEventLoop) {
544        use winit::event::ElementState;
545        use winit::keyboard::{Key, NamedKey};
546
547        // Check if any UI panel is visible
548        let any_ui_visible =
549            self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
550
551        // When UI panels are visible, block ALL keys from going to terminal
552        // except for UI control keys (Escape handled by egui, F1/F2/F3 for toggles)
553        if any_ui_visible {
554            let is_ui_control_key = matches!(
555                event.logical_key,
556                Key::Named(NamedKey::F1)
557                    | Key::Named(NamedKey::F2)
558                    | Key::Named(NamedKey::F3)
559                    | Key::Named(NamedKey::Escape)
560            );
561
562            if !is_ui_control_key {
563                log::debug!("Blocking key while UI visible: {:?}", event.logical_key);
564                return;
565            }
566        }
567
568        // Check if egui UI wants keyboard input (e.g., text fields, ComboBoxes)
569        if self.is_egui_using_keyboard() {
570            log::debug!("Blocking key event: egui wants keyboard input");
571            return;
572        }
573
574        // Check if shell has exited
575        let is_running = if let Some(terminal) = &self.terminal {
576            if let Ok(term) = terminal.try_lock() {
577                term.is_running()
578            } else {
579                true
580            }
581        } else {
582            true
583        };
584
585        // If shell exited and user presses any key, exit the application
586        // (fallback behavior if close_on_shell_exit is false)
587        if !is_running && event.state == ElementState::Pressed {
588            log::info!("Shell has exited, closing terminal on keypress");
589            // Abort the refresh task to prevent lockup on shutdown
590            if let Some(task) = self.refresh_task.take() {
591                task.abort();
592                log::info!("Refresh task aborted");
593            }
594            event_loop.exit();
595            return;
596        }
597
598        // Update last key press time for cursor blink reset
599        if event.state == ElementState::Pressed {
600            self.last_key_press = Some(std::time::Instant::now());
601        }
602
603        // Check if this is a scroll navigation key
604        if self.handle_scroll_keys(&event) {
605            return; // Key was handled for scrolling, don't send to terminal
606        }
607
608        // Check if this is a config reload key (F5)
609        if self.handle_config_reload(&event) {
610            return; // Key was handled for config reload, don't send to terminal
611        }
612
613        // Check if this is a clipboard history key (Ctrl+Shift+H)
614        if self.handle_clipboard_history_keys(&event) {
615            return; // Key was handled for clipboard history, don't send to terminal
616        }
617
618        // Check for fullscreen toggle (F11)
619        if self.handle_fullscreen_toggle(&event) {
620            return; // Key was handled for fullscreen toggle
621        }
622
623        // Check for help toggle (F1)
624        if self.handle_help_toggle(&event) {
625            return; // Key was handled for help toggle
626        }
627
628        // Check for settings toggle (F12)
629        if self.handle_settings_toggle(&event) {
630            return; // Key was handled for settings toggle
631        }
632
633        // Check for shader editor toggle (F11)
634        if self.handle_shader_editor_toggle(&event) {
635            return; // Key was handled for shader editor toggle
636        }
637
638        // Check for FPS overlay toggle (F3)
639        if self.handle_fps_overlay_toggle(&event) {
640            return; // Key was handled for FPS overlay toggle
641        }
642
643        // Check for utility shortcuts (clear scrollback, font size, etc.)
644        if self.handle_utility_shortcuts(&event, event_loop) {
645            return; // Key was handled by utility shortcut
646        }
647
648        // Clear selection on keyboard input (except for special keys handled above)
649        if event.state == ElementState::Pressed && self.selection.is_some() {
650            self.selection = None;
651            if let Some(window) = &self.window {
652                window.request_redraw();
653            }
654        }
655
656        // Debug: Log Tab and Space key before processing
657        let is_tab = matches!(event.logical_key, Key::Named(NamedKey::Tab));
658        let is_space = matches!(event.logical_key, Key::Named(NamedKey::Space));
659        if is_tab {
660            log::debug!("Tab key event received, state={:?}", event.state);
661        }
662        if is_space {
663            log::debug!("Space key event received, state={:?}", event.state);
664        }
665
666        // Normal key handling - send to terminal
667        if let Some(bytes) = self.input_handler.handle_key_event(event)
668            && let Some(terminal) = &self.terminal
669        {
670            if is_tab {
671                log::debug!("Sending Tab key to terminal ({} bytes)", bytes.len());
672            }
673            if is_space {
674                log::debug!("Sending Space key to terminal ({} bytes)", bytes.len());
675            }
676            let terminal_clone = Arc::clone(terminal);
677
678            self.runtime.spawn(async move {
679                let term = terminal_clone.lock().await;
680                let _ = term.write(&bytes);
681            });
682        }
683    }
684
685    fn handle_scroll_keys(&mut self, event: &KeyEvent) -> bool {
686        use winit::event::ElementState;
687        use winit::keyboard::{Key, NamedKey};
688
689        if event.state != ElementState::Pressed {
690            return false;
691        }
692
693        let shift = self.input_handler.modifiers.state().shift_key();
694
695        let handled = match &event.logical_key {
696            Key::Named(NamedKey::PageUp) => {
697                // Scroll up one page
698                self.scroll_up_page();
699                true
700            }
701            Key::Named(NamedKey::PageDown) => {
702                // Scroll down one page
703                self.scroll_down_page();
704                true
705            }
706            Key::Named(NamedKey::Home) if shift => {
707                // Shift+Home: Scroll to top
708                self.scroll_to_top();
709                true
710            }
711            Key::Named(NamedKey::End) if shift => {
712                // Shift+End: Scroll to bottom
713                self.scroll_to_bottom();
714                true
715            }
716            _ => false,
717        };
718
719        if handled && let Some(window) = &self.window {
720            window.request_redraw();
721        }
722
723        handled
724    }
725
726    fn handle_config_reload(&mut self, event: &KeyEvent) -> bool {
727        use winit::event::ElementState;
728        use winit::keyboard::{Key, NamedKey};
729
730        if event.state != ElementState::Pressed {
731            return false;
732        }
733
734        // F5 to reload config
735        if matches!(event.logical_key, Key::Named(NamedKey::F5)) {
736            log::info!("Reloading configuration (F5 pressed)");
737            self.reload_config();
738            return true;
739        }
740
741        false
742    }
743
744    fn reload_config(&mut self) {
745        match Config::load() {
746            Ok(new_config) => {
747                log::info!("Configuration reloaded successfully");
748
749                // Apply settings that can be changed at runtime
750
751                // Update auto_copy_selection
752                self.config.auto_copy_selection = new_config.auto_copy_selection;
753
754                // Update middle_click_paste
755                self.config.middle_click_paste = new_config.middle_click_paste;
756
757                // Update window title
758                if self.config.window_title != new_config.window_title {
759                    self.config.window_title = new_config.window_title.clone();
760                    if let Some(window) = &self.window {
761                        window.set_title(&new_config.window_title);
762                    }
763                }
764
765                // Update theme
766                if self.config.theme != new_config.theme {
767                    self.config.theme = new_config.theme.clone();
768                    if let Some(terminal) = &self.terminal
769                        && let Ok(mut term) = terminal.try_lock()
770                    {
771                        term.set_theme(new_config.load_theme());
772                        log::info!("Applied new theme: {}", new_config.theme);
773                    }
774                }
775
776                // Note: Clipboard history and notification settings not yet available in core library
777                // Config reloading for these features will be enabled when APIs become available
778
779                // Note: Terminal dimensions and scrollback size still require restart
780                if new_config.font_size != self.config.font_size {
781                    log::info!(
782                        "Font size changed from {} -> {} (applied live)",
783                        self.config.font_size,
784                        new_config.font_size
785                    );
786                }
787
788                if new_config.cols != self.config.cols || new_config.rows != self.config.rows {
789                    log::warn!("Terminal dimensions change requires restart");
790                }
791
792                // Request redraw to apply theme changes
793                if let Some(window) = &self.window {
794                    window.request_redraw();
795                }
796            }
797            Err(e) => {
798                log::error!("Failed to reload configuration: {}", e);
799            }
800        }
801    }
802
803    fn handle_clipboard_history_keys(&mut self, event: &KeyEvent) -> bool {
804        use winit::event::ElementState;
805        use winit::keyboard::Key;
806
807        // Handle Escape to close clipboard history UI
808        if self.clipboard_history_ui.visible {
809            if event.state == ElementState::Pressed {
810                match &event.logical_key {
811                    Key::Named(winit::keyboard::NamedKey::Escape) => {
812                        self.clipboard_history_ui.visible = false;
813                        self.needs_redraw = true;
814                        return true;
815                    }
816                    Key::Named(winit::keyboard::NamedKey::ArrowUp) => {
817                        self.clipboard_history_ui.select_previous();
818                        self.needs_redraw = true;
819                        return true;
820                    }
821                    Key::Named(winit::keyboard::NamedKey::ArrowDown) => {
822                        self.clipboard_history_ui.select_next();
823                        self.needs_redraw = true;
824                        return true;
825                    }
826                    Key::Named(winit::keyboard::NamedKey::Enter) => {
827                        // Paste the selected entry
828                        if let Some(entry) = self.clipboard_history_ui.selected_entry() {
829                            let content = entry.content.clone();
830                            self.clipboard_history_ui.visible = false;
831                            self.paste_text(&content);
832                            self.needs_redraw = true;
833                        }
834                        return true;
835                    }
836                    _ => {}
837                }
838            }
839            // While clipboard history is visible, consume all key events
840            return true;
841        }
842
843        // Ctrl+Shift+H: Toggle clipboard history UI
844        if event.state == ElementState::Pressed {
845            let ctrl = self.input_handler.modifiers.state().control_key();
846            let shift = self.input_handler.modifiers.state().shift_key();
847
848            if ctrl
849                && shift
850                && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "h" || c.as_str() == "H")
851            {
852                self.toggle_clipboard_history();
853                return true;
854            }
855        }
856
857        false
858    }
859
860    fn toggle_clipboard_history(&mut self) {
861        // Refresh clipboard history entries from terminal before showing
862        if let Some(terminal) = &self.terminal
863            && let Ok(term) = terminal.try_lock()
864        {
865            // Get history for all slots and merge
866            let mut all_entries = Vec::new();
867            all_entries.extend(term.get_clipboard_history(ClipboardSlot::Primary));
868            all_entries.extend(term.get_clipboard_history(ClipboardSlot::Clipboard));
869            all_entries.extend(term.get_clipboard_history(ClipboardSlot::Selection));
870
871            // Sort by timestamp (newest first)
872            all_entries.sort_by_key(|e| std::cmp::Reverse(e.timestamp));
873
874            self.clipboard_history_ui.update_entries(all_entries);
875        }
876
877        self.clipboard_history_ui.toggle();
878        self.needs_redraw = true;
879        log::debug!(
880            "Clipboard history UI toggled: {}",
881            self.clipboard_history_ui.visible
882        );
883    }
884
885    fn paste_text(&mut self, text: &str) {
886        if let Some(terminal) = &self.terminal {
887            let terminal_clone = Arc::clone(terminal);
888            // Convert newlines to carriage returns for terminal
889            let text = text.replace('\n', "\r");
890            self.runtime.spawn(async move {
891                let term = terminal_clone.lock().await;
892                let _ = term.write(text.as_bytes());
893                log::debug!("Pasted text from clipboard history ({} bytes)", text.len());
894            });
895        }
896    }
897
898    /// Force surface reconfiguration - useful when rendering becomes corrupted
899    /// after moving between monitors or when automatic detection fails.
900    /// Also clears glyph cache to ensure fonts render correctly.
901    fn force_surface_reconfigure(&mut self) {
902        log::info!("Force surface reconfigure triggered");
903
904        if let Some(renderer) = &mut self.renderer {
905            // Reconfigure the surface
906            renderer.reconfigure_surface();
907
908            // Clear glyph cache to force re-rasterization at correct DPI
909            renderer.clear_glyph_cache();
910
911            // Invalidate cached cells to force full re-render
912            self.cached_cells = None;
913        }
914
915        // On macOS, reconfigure the Metal layer
916        #[cfg(target_os = "macos")]
917        {
918            if let Some(window) = &self.window
919                && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
920            {
921                log::warn!("Failed to reconfigure Metal layer: {}", e);
922            }
923        }
924
925        // Request redraw
926        if let Some(window) = &self.window {
927            window.request_redraw();
928        }
929
930        self.needs_redraw = true;
931    }
932
933    fn handle_utility_shortcuts(
934        &mut self,
935        event: &KeyEvent,
936        _event_loop: &ActiveEventLoop,
937    ) -> bool {
938        use winit::event::ElementState;
939        use winit::keyboard::Key;
940
941        if event.state != ElementState::Pressed {
942            return false;
943        }
944
945        let ctrl = self.input_handler.modifiers.state().control_key();
946        let shift = self.input_handler.modifiers.state().shift_key();
947
948        // Ctrl+Shift+K: Clear scrollback
949        if ctrl
950            && shift
951            && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "k" || c.as_str() == "K")
952        {
953            // Clear scrollback if terminal is available
954            let cleared = if let Some(terminal) = &self.terminal
955                && let Ok(term) = terminal.try_lock()
956            {
957                term.clear_scrollback();
958                true
959            } else {
960                false
961            };
962
963            if cleared {
964                self.cached_scrollback_len = 0;
965                self.set_scroll_target(0);
966                log::info!("Cleared scrollback buffer");
967            }
968            return true;
969        }
970
971        // Ctrl+L: Clear screen (send clear sequence to shell)
972        if ctrl
973            && !shift
974            && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "l" || c.as_str() == "L")
975        {
976            if let Some(terminal) = &self.terminal {
977                let terminal_clone = Arc::clone(terminal);
978                // Send the "clear" command sequence (Ctrl+L)
979                let clear_sequence = vec![0x0C]; // Ctrl+L character
980                self.runtime.spawn(async move {
981                    if let Ok(term) = terminal_clone.try_lock() {
982                        let _ = term.write(&clear_sequence);
983                        log::debug!("Sent clear screen sequence (Ctrl+L)");
984                    }
985                });
986            }
987            return true;
988        }
989
990        // Ctrl+Plus/Equals: Increase font size (applies live)
991        if ctrl
992            && !shift
993            && (matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "+" || c.as_str() == "="))
994        {
995            self.config.font_size = (self.config.font_size + 1.0).min(72.0);
996            self.pending_font_rebuild = true;
997            log::info!(
998                "Font size increased to {} (applying live)",
999                self.config.font_size
1000            );
1001            if let Some(window) = &self.window {
1002                window.request_redraw();
1003            }
1004            return true;
1005        }
1006
1007        // Ctrl+Minus: Decrease font size (applies live)
1008        if ctrl
1009            && !shift
1010            && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "-" || c.as_str() == "_")
1011        {
1012            self.config.font_size = (self.config.font_size - 1.0).max(6.0);
1013            self.pending_font_rebuild = true;
1014            log::info!(
1015                "Font size decreased to {} (applying live)",
1016                self.config.font_size
1017            );
1018            if let Some(window) = &self.window {
1019                window.request_redraw();
1020            }
1021            return true;
1022        }
1023
1024        // Ctrl+0: Reset font size to default (applies live)
1025        if ctrl && !shift && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "0")
1026        {
1027            self.config.font_size = 14.0; // Default font size
1028            self.pending_font_rebuild = true;
1029            log::info!("Font size reset to default (14.0, applying live)");
1030            if let Some(window) = &self.window {
1031                window.request_redraw();
1032            }
1033            return true;
1034        }
1035
1036        // Ctrl+, (Cmd+, on macOS): Cycle cursor style (Block -> Beam -> Underline -> Block)
1037        let super_key = self.input_handler.modifiers.state().super_key();
1038        let ctrl_or_cmd = ctrl || super_key;
1039
1040        if ctrl_or_cmd
1041            && !shift
1042            && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == ",")
1043        {
1044            use crate::config::CursorStyle;
1045            use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
1046
1047            // Cycle to next cursor style
1048            self.config.cursor_style = match self.config.cursor_style {
1049                CursorStyle::Block => CursorStyle::Beam,
1050                CursorStyle::Beam => CursorStyle::Underline,
1051                CursorStyle::Underline => CursorStyle::Block,
1052            };
1053
1054            log::info!("Cursor style changed to {:?}", self.config.cursor_style);
1055
1056            // Apply to terminal
1057            if let Some(terminal_mgr) = &self.terminal
1058                && let Ok(term_mgr) = terminal_mgr.try_lock()
1059            {
1060                let terminal = term_mgr.terminal();
1061                if let Some(mut term) = terminal.try_lock() {
1062                    let term_style = match self.config.cursor_style {
1063                        CursorStyle::Block => TermCursorStyle::SteadyBlock,
1064                        CursorStyle::Beam => TermCursorStyle::SteadyBar,
1065                        CursorStyle::Underline => TermCursorStyle::SteadyUnderline,
1066                    };
1067                    term.set_cursor_style(term_style);
1068                }
1069            }
1070
1071            // Force redraw to reflect cursor style change
1072            self.cached_cells = None;
1073            self.last_cursor_pos = None;
1074            if let Some(window) = &self.window {
1075                window.request_redraw();
1076            }
1077            return true;
1078        }
1079
1080        // Ctrl+Shift+S: Take screenshot
1081        if ctrl
1082            && shift
1083            && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "s" || c.as_str() == "S")
1084        {
1085            self.take_screenshot();
1086            return true;
1087        }
1088
1089        // Ctrl+Shift+R: Toggle session recording
1090        if ctrl
1091            && shift
1092            && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "r" || c.as_str() == "R")
1093        {
1094            self.toggle_recording();
1095            return true;
1096        }
1097
1098        // Ctrl+Shift+F5: Force surface reconfigure (fixes rendering corruption)
1099        if ctrl && shift && matches!(event.logical_key, Key::Named(winit::keyboard::NamedKey::F5)) {
1100            log::info!("Manual surface reconfigure triggered via Ctrl+Shift+F5");
1101            self.force_surface_reconfigure();
1102            return true;
1103        }
1104
1105        false
1106    }
1107
1108    fn handle_fullscreen_toggle(&mut self, event: &KeyEvent) -> bool {
1109        use winit::event::ElementState;
1110        use winit::keyboard::{Key, NamedKey};
1111
1112        if event.state != ElementState::Pressed {
1113            return false;
1114        }
1115
1116        // F11: Toggle fullscreen
1117        if matches!(event.logical_key, Key::Named(NamedKey::F11))
1118            && let Some(window) = &self.window
1119        {
1120            self.is_fullscreen = !self.is_fullscreen;
1121
1122            if self.is_fullscreen {
1123                window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
1124                log::info!("Entering fullscreen mode");
1125            } else {
1126                window.set_fullscreen(None);
1127                log::info!("Exiting fullscreen mode");
1128            }
1129
1130            return true;
1131        }
1132
1133        false
1134    }
1135
1136    fn handle_settings_toggle(&mut self, event: &KeyEvent) -> bool {
1137        use winit::event::ElementState;
1138        use winit::keyboard::{Key, NamedKey};
1139
1140        if event.state != ElementState::Pressed {
1141            return false;
1142        }
1143
1144        // F12: Toggle settings UI
1145        if matches!(event.logical_key, Key::Named(NamedKey::F12)) {
1146            self.settings_ui.toggle();
1147            log::info!(
1148                "Settings UI toggled: {}",
1149                if self.settings_ui.visible {
1150                    "visible"
1151                } else {
1152                    "hidden"
1153                }
1154            );
1155
1156            // Request redraw to show/hide settings
1157            if let Some(window) = &self.window {
1158                window.request_redraw();
1159            }
1160
1161            return true;
1162        }
1163
1164        false
1165    }
1166
1167    /// Handle F1 key to toggle help panel
1168    fn handle_help_toggle(&mut self, event: &KeyEvent) -> bool {
1169        use winit::event::ElementState;
1170        use winit::keyboard::{Key, NamedKey};
1171
1172        if event.state != ElementState::Pressed {
1173            return false;
1174        }
1175
1176        // F1: Toggle help UI
1177        if matches!(event.logical_key, Key::Named(NamedKey::F1)) {
1178            self.help_ui.toggle();
1179            log::info!(
1180                "Help UI toggled: {}",
1181                if self.help_ui.visible {
1182                    "visible"
1183                } else {
1184                    "hidden"
1185                }
1186            );
1187
1188            // Request redraw to show/hide help
1189            if let Some(window) = &self.window {
1190                window.request_redraw();
1191            }
1192
1193            return true;
1194        }
1195
1196        // Escape: Close help UI if visible
1197        if matches!(event.logical_key, Key::Named(NamedKey::Escape)) && self.help_ui.visible {
1198            self.help_ui.visible = false;
1199            log::info!("Help UI closed via Escape");
1200
1201            if let Some(window) = &self.window {
1202                window.request_redraw();
1203            }
1204
1205            return true;
1206        }
1207
1208        false
1209    }
1210
1211    /// Handle F11 key to toggle shader editor
1212    fn handle_shader_editor_toggle(&mut self, event: &KeyEvent) -> bool {
1213        use winit::event::ElementState;
1214        use winit::keyboard::{Key, NamedKey};
1215
1216        if event.state != ElementState::Pressed {
1217            return false;
1218        }
1219
1220        // F11: Toggle shader editor
1221        if matches!(event.logical_key, Key::Named(NamedKey::F11)) {
1222            if self.settings_ui.is_shader_editor_visible() {
1223                // Close shader editor - handled by the UI itself
1224                log::info!("Shader editor close requested via F11");
1225            } else {
1226                // Open shader editor
1227                if self.settings_ui.open_shader_editor() {
1228                    log::info!("Shader editor opened via F11");
1229                } else {
1230                    log::warn!("Cannot open shader editor: no shader path configured in settings");
1231                }
1232            }
1233
1234            // Request redraw to show/hide shader editor
1235            if let Some(window) = &self.window {
1236                window.request_redraw();
1237            }
1238
1239            return true;
1240        }
1241
1242        false
1243    }
1244
1245    /// Handle F3 key to toggle FPS overlay
1246    fn handle_fps_overlay_toggle(&mut self, event: &KeyEvent) -> bool {
1247        use winit::event::ElementState;
1248        use winit::keyboard::{Key, NamedKey};
1249
1250        if event.state != ElementState::Pressed {
1251            return false;
1252        }
1253
1254        // F3: Toggle FPS overlay
1255        if matches!(event.logical_key, Key::Named(NamedKey::F3)) {
1256            self.show_fps_overlay = !self.show_fps_overlay;
1257            log::info!(
1258                "FPS overlay toggled: {}",
1259                if self.show_fps_overlay {
1260                    "visible"
1261                } else {
1262                    "hidden"
1263                }
1264            );
1265
1266            // Request redraw to show/hide FPS overlay
1267            if let Some(window) = &self.window {
1268                window.request_redraw();
1269            }
1270
1271            return true;
1272        }
1273
1274        false
1275    }
1276
1277    fn scroll_up_page(&mut self) {
1278        // Calculate page size based on visible lines
1279        if let Some(renderer) = &self.renderer {
1280            let char_height = self.config.font_size * 1.2;
1281            let page_size = (renderer.size().height as f32 / char_height) as usize;
1282
1283            let new_target = self.scroll_state.target_offset.saturating_add(page_size);
1284            let clamped_target = new_target.min(self.cached_scrollback_len);
1285            self.set_scroll_target(clamped_target);
1286        }
1287    }
1288
1289    fn scroll_down_page(&mut self) {
1290        // Calculate page size based on visible lines
1291        if let Some(renderer) = &self.renderer {
1292            let char_height = self.config.font_size * 1.2;
1293            let page_size = (renderer.size().height as f32 / char_height) as usize;
1294
1295            let new_target = self.scroll_state.target_offset.saturating_sub(page_size);
1296            self.set_scroll_target(new_target);
1297        }
1298    }
1299
1300    fn scroll_to_top(&mut self) {
1301        self.set_scroll_target(self.cached_scrollback_len);
1302    }
1303
1304    fn scroll_to_bottom(&mut self) {
1305        self.set_scroll_target(0);
1306    }
1307
1308    /// Check if egui is currently using the pointer (mouse is over an egui UI element)
1309    fn is_egui_using_pointer(&self) -> bool {
1310        // If any UI panel is visible, check if egui wants the pointer
1311        let any_ui_visible =
1312            self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
1313        if !any_ui_visible {
1314            return false;
1315        }
1316
1317        // Check egui context for pointer usage
1318        if let Some(ctx) = &self.egui_ctx {
1319            ctx.is_using_pointer() || ctx.wants_pointer_input()
1320        } else {
1321            false
1322        }
1323    }
1324
1325    /// Check if egui is currently using keyboard input (e.g., text input or ComboBox has focus)
1326    fn is_egui_using_keyboard(&self) -> bool {
1327        // If any UI panel is visible, check if egui wants keyboard input
1328        let any_ui_visible =
1329            self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
1330        if !any_ui_visible {
1331            return false;
1332        }
1333
1334        // Check egui context for keyboard usage
1335        if let Some(ctx) = &self.egui_ctx {
1336            ctx.wants_keyboard_input()
1337        } else {
1338            false
1339        }
1340    }
1341
1342    fn handle_mouse_wheel(&mut self, delta: MouseScrollDelta) {
1343        // --- 1. Mouse Tracking Protocol ---
1344        // Check if the terminal application (e.g., vim, htop) has requested mouse tracking.
1345        // If enabled, we forward wheel events to the PTY instead of scrolling locally.
1346        if let Some(terminal) = &self.terminal
1347            && let Ok(term) = terminal.try_lock()
1348            && term.is_mouse_tracking_enabled()
1349        {
1350            // Calculate scroll lines based on delta type (Line vs Pixel)
1351            let scroll_lines = match delta {
1352                MouseScrollDelta::LineDelta(_x, y) => y as i32,
1353                MouseScrollDelta::PixelDelta(pos) => (pos.y / 20.0) as i32,
1354            };
1355
1356            // Map pixel position to terminal cell coordinates
1357            if let Some((col, row)) =
1358                self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1359            {
1360                // XTerm mouse protocol buttons: 64 = scroll up, 65 = scroll down
1361                let button = if scroll_lines > 0 { 64 } else { 65 };
1362                // Limit burst to 10 events to avoid flooding the PTY
1363                let count = scroll_lines.unsigned_abs().min(10);
1364
1365                // Encode and send to terminal via async task
1366                let mut all_encoded = Vec::new();
1367                for _ in 0..count {
1368                    let encoded = term.encode_mouse_event(button, col, row, true, 0);
1369                    if !encoded.is_empty() {
1370                        all_encoded.extend(encoded);
1371                    }
1372                }
1373
1374                if !all_encoded.is_empty() {
1375                    let terminal_clone = Arc::clone(terminal);
1376                    let runtime = Arc::clone(&self.runtime);
1377                    runtime.spawn(async move {
1378                        let t = terminal_clone.lock().await;
1379                        let _ = t.write(&all_encoded);
1380                    });
1381                }
1382            }
1383            return; // Exit early: terminal app handled the input
1384        }
1385
1386        // --- 2. Local Scrolling ---
1387        // Normal behavior: scroll through the local scrollback buffer.
1388        let scroll_lines = match delta {
1389            MouseScrollDelta::LineDelta(_x, y) => (y * self.config.mouse_scroll_speed) as i32,
1390            MouseScrollDelta::PixelDelta(pos) => (pos.y / 20.0) as i32,
1391        };
1392
1393        let scrollback_len = self.cached_scrollback_len;
1394
1395        // Calculate new scroll target (positive delta = scroll up = increase offset)
1396        let new_target = self.scroll_state.apply_scroll(scroll_lines, scrollback_len);
1397
1398        // Update target and trigger interpolation animation
1399        self.set_scroll_target(new_target);
1400    }
1401
1402    /// Set scroll target and initiate smooth interpolation animation.
1403    /// The actual interpolation happens in `update_scroll_animation` during render.
1404    fn set_scroll_target(&mut self, new_offset: usize) {
1405        if self.scroll_state.set_target(new_offset) {
1406            // Request redraw to start the animation loop
1407
1408            if let Some(window) = &self.window {
1409                window.request_redraw();
1410            }
1411        }
1412    }
1413
1414    /// Update smooth scroll animation via interpolation.
1415    /// Returns true if the animation is still in progress.
1416    fn update_scroll_animation(&mut self) -> bool {
1417        self.scroll_state.update_animation()
1418    }
1419
1420    /// Convert pixel coordinates to terminal cell coordinates
1421    fn pixel_to_cell(&self, x: f64, y: f64) -> Option<(usize, usize)> {
1422        if let Some(renderer) = &self.renderer {
1423            // Use actual cell dimensions from renderer for accurate coordinate mapping
1424            let cell_width = renderer.cell_width() as f64;
1425            let cell_height = renderer.cell_height() as f64;
1426            let padding = renderer.window_padding() as f64;
1427
1428            // Account for window padding (all sides)
1429            let adjusted_x = (x - padding).max(0.0);
1430            let adjusted_y = (y - padding).max(0.0);
1431
1432            let col = (adjusted_x / cell_width) as usize;
1433            let row = (adjusted_y / cell_height) as usize;
1434
1435            Some((col, row))
1436        } else {
1437            None
1438        }
1439    }
1440
1441    /// Determine if scrollbar should be visible based on autohide setting and recent activity
1442    fn should_show_scrollbar(&self) -> bool {
1443        // No scrollbar needed if no scrollback available
1444        if self.cached_scrollback_len == 0 {
1445            return false;
1446        }
1447
1448        // Always show when dragging or moving
1449        if self.scroll_state.dragging {
1450            return true;
1451        }
1452
1453        // If autohide disabled, always show
1454        if self.config.scrollbar_autohide_delay == 0 {
1455            return true;
1456        }
1457
1458        // If scrolled away from bottom, keep visible
1459        if self.scroll_state.offset > 0 || self.scroll_state.target_offset > 0 {
1460            return true;
1461        }
1462
1463        // Show when pointer is near the scrollbar edge (hover reveal)
1464        if let Some(window) = &self.window {
1465            let padding = 32.0; // px hover band
1466            let width = window.inner_size().width as f64;
1467            let near_right = self.config.scrollbar_position != "left"
1468                && (width - self.mouse_position.0) <= padding;
1469            let near_left =
1470                self.config.scrollbar_position == "left" && self.mouse_position.0 <= padding;
1471            if near_left || near_right {
1472                return true;
1473            }
1474        }
1475
1476        // Otherwise, hide after delay
1477        self.scroll_state.last_activity.elapsed().as_millis()
1478            < self.config.scrollbar_autohide_delay as u128
1479    }
1480
1481    /// Select word at the given position
1482    fn select_word_at(&mut self, col: usize, row: usize) {
1483        if let Some(terminal) = &self.terminal
1484            && let Ok(term) = terminal.try_lock()
1485        {
1486            let (cols, _rows) = term.dimensions();
1487            let visible_cells =
1488                term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1489            if visible_cells.is_empty() || cols == 0 {
1490                return;
1491            }
1492
1493            let cell_idx = row * cols + col;
1494            if cell_idx >= visible_cells.len() {
1495                return;
1496            }
1497
1498            // Find word boundaries
1499            let mut start_col = col;
1500            let mut end_col = col;
1501
1502            // Expand left
1503            for c in (0..col).rev() {
1504                let idx = row * cols + c;
1505                if idx >= visible_cells.len() {
1506                    break;
1507                }
1508                let ch = visible_cells[idx].grapheme.chars().next().unwrap_or('\0');
1509                if ch.is_alphanumeric() || ch == '_' {
1510                    start_col = c;
1511                } else {
1512                    break;
1513                }
1514            }
1515
1516            // Expand right
1517            for c in col..cols {
1518                let idx = row * cols + c;
1519                if idx >= visible_cells.len() {
1520                    break;
1521                }
1522                let ch = visible_cells[idx].grapheme.chars().next().unwrap_or('\0');
1523                if ch.is_alphanumeric() || ch == '_' {
1524                    end_col = c;
1525                } else {
1526                    break;
1527                }
1528            }
1529
1530            self.selection = Some(Selection::new(
1531                (start_col, row),
1532                (end_col, row),
1533                SelectionMode::Normal,
1534            ));
1535        }
1536    }
1537
1538    /// Select entire line at the given row (used for triple-click)
1539    fn select_line_at(&mut self, row: usize) {
1540        if let Some(terminal) = &self.terminal
1541            && let Ok(term) = terminal.try_lock()
1542        {
1543            let (cols, _rows) = term.dimensions();
1544            if cols == 0 {
1545                return;
1546            }
1547
1548            // Store the row in start/end - Line mode uses rows only
1549            self.selection = Some(Selection::new(
1550                (0, row),
1551                (cols.saturating_sub(1), row),
1552                SelectionMode::Line,
1553            ));
1554        }
1555    }
1556
1557    /// Extend line selection to include rows from anchor to current row
1558    fn extend_line_selection(&mut self, current_row: usize) {
1559        if let Some(terminal) = &self.terminal
1560            && let Ok(term) = terminal.try_lock()
1561        {
1562            let (cols, _rows) = term.dimensions();
1563            if cols == 0 {
1564                return;
1565            }
1566
1567            // Use click_position as the anchor row (the originally triple-clicked row)
1568            let anchor_row = self.click_position.map(|(_, r)| r).unwrap_or(current_row);
1569
1570            if let Some(ref mut selection) = self.selection
1571                && selection.mode == SelectionMode::Line
1572            {
1573                // For line selection, always ensure full lines are selected
1574                // by setting columns appropriately based on drag direction
1575                if current_row >= anchor_row {
1576                    // Dragging down or same row: start at col 0, end at last col
1577                    selection.start = (0, anchor_row);
1578                    selection.end = (cols.saturating_sub(1), current_row);
1579                } else {
1580                    // Dragging up: start at last col (anchor row), end at col 0 (current row)
1581                    // After normalization, this becomes: start=(0, current_row), end=(cols-1, anchor_row)
1582                    selection.start = (cols.saturating_sub(1), anchor_row);
1583                    selection.end = (0, current_row);
1584                }
1585            }
1586        }
1587    }
1588
1589    /// Extract selected text from terminal
1590    fn get_selected_text(&self) -> Option<String> {
1591        if let (Some(selection), Some(terminal)) = (&self.selection, &self.terminal) {
1592            if let Ok(term) = terminal.try_lock() {
1593                let (start, end) = selection.normalized();
1594                let (start_col, start_row) = start;
1595                let (end_col, end_row) = end;
1596
1597                let (cols, rows) = term.dimensions();
1598                let visible_cells =
1599                    term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1600                if visible_cells.is_empty() || cols == 0 {
1601                    return None;
1602                }
1603
1604                let mut visible_lines = Vec::with_capacity(rows);
1605                for row in 0..rows {
1606                    let start_idx = row * cols;
1607                    let end_idx = start_idx.saturating_add(cols);
1608                    if end_idx > visible_cells.len() {
1609                        break;
1610                    }
1611
1612                    let mut line = String::with_capacity(cols);
1613                    for cell in &visible_cells[start_idx..end_idx] {
1614                        line.push_str(&cell.grapheme);
1615                    }
1616                    visible_lines.push(line);
1617                }
1618
1619                if visible_lines.is_empty() {
1620                    return None;
1621                }
1622
1623                let mut selected_text = String::new();
1624                let max_row = visible_lines.len().saturating_sub(1);
1625                let start_row = start_row.min(max_row);
1626                let end_row = end_row.min(max_row);
1627
1628                if selection.mode == SelectionMode::Line {
1629                    // Line selection: extract full lines
1630                    #[allow(clippy::needless_range_loop)]
1631                    for row in start_row..=end_row {
1632                        if row > start_row {
1633                            selected_text.push('\n');
1634                        }
1635                        let line = &visible_lines[row];
1636                        // Trim trailing spaces from each line but keep the content
1637                        selected_text.push_str(line.trim_end());
1638                    }
1639                } else if selection.mode == SelectionMode::Rectangular {
1640                    // Rectangular selection: extract same columns from each row
1641                    let min_col = start_col.min(end_col);
1642                    let max_col = start_col.max(end_col);
1643
1644                    #[allow(clippy::needless_range_loop)]
1645                    for row in start_row..=end_row {
1646                        if row > start_row {
1647                            selected_text.push('\n');
1648                        }
1649                        let line = &visible_lines[row];
1650                        selected_text.push_str(&Self::extract_columns(
1651                            line,
1652                            min_col,
1653                            Some(max_col),
1654                        ));
1655                    }
1656                } else if start_row == end_row {
1657                    // Normal single-line selection
1658                    let line = &visible_lines[start_row];
1659                    selected_text = Self::extract_columns(line, start_col, Some(end_col));
1660                } else {
1661                    // Normal multi-line selection
1662                    for (idx, row) in (start_row..=end_row).enumerate() {
1663                        let line = &visible_lines[row];
1664                        if idx == 0 {
1665                            selected_text.push_str(&Self::extract_columns(line, start_col, None));
1666                        } else if row == end_row {
1667                            selected_text.push('\n');
1668                            selected_text.push_str(&Self::extract_columns(line, 0, Some(end_col)));
1669                        } else {
1670                            selected_text.push('\n');
1671                            selected_text.push_str(line);
1672                        }
1673                    }
1674                }
1675
1676                Some(selected_text)
1677            } else {
1678                None
1679            }
1680        } else {
1681            None
1682        }
1683    }
1684
1685    /// Detect URLs in the visible terminal area (both regex-detected and OSC 8 hyperlinks)
1686    fn detect_urls(&mut self) {
1687        self.detected_urls.clear();
1688
1689        if let Some(terminal) = &self.terminal
1690            && let Ok(term) = terminal.try_lock()
1691        {
1692            let (cols, rows) = term.dimensions();
1693            let visible_cells =
1694                term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1695
1696            if visible_cells.is_empty() || cols == 0 {
1697                return;
1698            }
1699
1700            // Build hyperlink ID to URL mapping from terminal
1701            let mut hyperlink_urls = std::collections::HashMap::new();
1702            let all_hyperlinks = term.get_all_hyperlinks();
1703            for hyperlink_info in all_hyperlinks {
1704                // Get the hyperlink ID from the first position
1705                if let Some((col, row)) = hyperlink_info.positions.first() {
1706                    // Get the cell at this position to find the hyperlink_id
1707                    let cell_idx = row * cols + col;
1708                    if let Some(cell) = visible_cells.get(cell_idx)
1709                        && let Some(id) = cell.hyperlink_id
1710                    {
1711                        hyperlink_urls.insert(id, hyperlink_info.url.clone());
1712                    }
1713                }
1714            }
1715
1716            // Extract text from each visible line and detect URLs
1717            for row in 0..rows {
1718                let start_idx = row * cols;
1719                let end_idx = start_idx.saturating_add(cols);
1720                if end_idx > visible_cells.len() {
1721                    break;
1722                }
1723
1724                let row_cells = &visible_cells[start_idx..end_idx];
1725
1726                let mut line = String::with_capacity(cols);
1727                for cell in row_cells {
1728                    line.push_str(&cell.grapheme);
1729                }
1730
1731                // Adjust row to account for scroll offset
1732                let absolute_row = row + self.scroll_state.offset;
1733
1734                // Detect regex-based URLs in this line
1735                let regex_urls = url_detection::detect_urls_in_line(&line, absolute_row);
1736                self.detected_urls.extend(regex_urls);
1737
1738                // Detect OSC 8 hyperlinks in this row
1739                let osc8_urls =
1740                    url_detection::detect_osc8_hyperlinks(row_cells, absolute_row, &hyperlink_urls);
1741                self.detected_urls.extend(osc8_urls);
1742            }
1743        }
1744    }
1745
1746    /// Apply visual styling to cells that are part of detected URLs
1747    /// Changes the foreground color to indicate clickable URLs
1748    fn apply_url_underlines(
1749        &self,
1750        cells: &mut [crate::cell_renderer::Cell],
1751        renderer_size: &winit::dpi::PhysicalSize<u32>,
1752    ) {
1753        if self.detected_urls.is_empty() {
1754            return;
1755        }
1756
1757        // Calculate grid dimensions from renderer size
1758        let char_width = self.config.font_size * 0.6;
1759        let cols = (renderer_size.width as f32 / char_width) as usize;
1760
1761        // URL color: bright cyan (#4FC3F7) for visibility
1762        let url_color = [79, 195, 247, 255];
1763
1764        // Apply color styling to cells that are part of URLs
1765        for url in &self.detected_urls {
1766            // Convert absolute row (with scroll offset) to viewport-relative row
1767            if url.row < self.scroll_state.offset {
1768                continue; // URL is above the visible area
1769            }
1770            let viewport_row = url.row - self.scroll_state.offset;
1771
1772            // Calculate cell indices for this URL
1773            for col in url.start_col..url.end_col {
1774                let cell_idx = viewport_row * cols + col;
1775                if cell_idx < cells.len() {
1776                    cells[cell_idx].fg_color = url_color;
1777                    cells[cell_idx].underline = true; // Set for future underline rendering support
1778                }
1779            }
1780        }
1781    }
1782
1783    /// Send mouse event to terminal if mouse tracking is enabled
1784    ///
1785    /// Returns true if event was consumed by terminal (mouse tracking enabled or alt screen active),
1786    /// false otherwise. When on alt screen, we don't want local text selection.
1787    fn try_send_mouse_event(&self, button: u8, pressed: bool) -> bool {
1788        if let Some(terminal) = &self.terminal
1789            && let Some((col, row)) =
1790                self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1791            && let Ok(term) = terminal.try_lock()
1792        {
1793            // Check if alternate screen is active - don't do local selection on alt screen
1794            // even if mouse tracking isn't enabled (e.g., some TUI apps don't enable mouse)
1795            let alt_screen_active = term.is_alt_screen_active();
1796
1797            // Check if mouse tracking is enabled
1798            if term.is_mouse_tracking_enabled() {
1799                // Encode mouse event
1800                let encoded = term.encode_mouse_event(button, col, row, pressed, 0);
1801
1802                if !encoded.is_empty() {
1803                    // Send to PTY using async lock to ensure write completes
1804                    let terminal_clone = Arc::clone(terminal);
1805                    let runtime = Arc::clone(&self.runtime);
1806                    runtime.spawn(async move {
1807                        let t = terminal_clone.lock().await;
1808                        let _ = t.write(&encoded);
1809                    });
1810                }
1811                return true; // Event consumed by mouse tracking
1812            }
1813
1814            // On alt screen without mouse tracking - still consume event to prevent selection
1815            if alt_screen_active {
1816                return true;
1817            }
1818        }
1819        false // Event not consumed, handle normally
1820    }
1821
1822    /// Update cursor blink state based on configured interval
1823    fn update_cursor_blink(&mut self) {
1824        if !self.config.cursor_blink {
1825            // Smoothly fade to full visibility if blinking disabled
1826            self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1827            return;
1828        }
1829
1830        let now = std::time::Instant::now();
1831
1832        // If key was pressed recently (within 500ms), smoothly fade in cursor and reset blink timer
1833        if let Some(last_key) = self.last_key_press
1834            && now.duration_since(last_key).as_millis() < 500
1835        {
1836            self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1837            self.last_cursor_blink = Some(now);
1838            return;
1839        }
1840
1841        // Smooth cursor blink animation using sine wave for natural fade
1842        let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
1843
1844        if let Some(last_blink) = self.last_cursor_blink {
1845            let elapsed = now.duration_since(last_blink);
1846            let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
1847
1848            // Use cosine wave for smooth fade in/out (starts at 1.0, fades to 0.0, back to 1.0)
1849            self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
1850                .abs()
1851                .clamp(0.0, 1.0);
1852
1853            // Reset timer after full cycle (2x interval for full on+off)
1854            if elapsed >= blink_interval * 2 {
1855                self.last_cursor_blink = Some(now);
1856            }
1857        } else {
1858            // First time, start the blink timer with cursor fully visible
1859            self.cursor_opacity = 1.0;
1860            self.last_cursor_blink = Some(now);
1861        }
1862    }
1863
1864    fn handle_mouse_button(&mut self, button: MouseButton, state: ElementState) {
1865        // Track button press state for motion tracking logic (drag selection, motion reporting)
1866        self.mouse_button_pressed = state == ElementState::Pressed;
1867
1868        // --- 1. Shader Interaction ---
1869        // Update shader mouse state for left button (matches Shadertoy iMouse convention)
1870        if button == MouseButton::Left
1871            && let Some(ref mut renderer) = self.renderer
1872        {
1873            renderer.set_shader_mouse_button(
1874                state == ElementState::Pressed,
1875                self.mouse_position.0 as f32,
1876                self.mouse_position.1 as f32,
1877            );
1878        }
1879
1880        match button {
1881            MouseButton::Left => {
1882                // --- 2. URL Clicking ---
1883                // Check for Ctrl+Click on URL to open it in default browser
1884                if state == ElementState::Pressed
1885                    && self.input_handler.modifiers.state().control_key()
1886                    && let Some((col, row)) =
1887                        self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1888                {
1889                    // Adjust row for scroll offset
1890                    let adjusted_row = row + self.scroll_state.offset;
1891
1892                    if let Some(url) =
1893                        url_detection::find_url_at_position(&self.detected_urls, col, adjusted_row)
1894                    {
1895                        if let Err(e) = url_detection::open_url(&url.url) {
1896                            log::error!("Failed to open URL: {}", e);
1897                        }
1898                        return; // Exit early: URL click handled
1899                    }
1900                }
1901
1902                // --- 3. Mouse Tracking Forwarding ---
1903                // Forward events to the PTY if terminal application requested tracking
1904                if self.try_send_mouse_event(0, state == ElementState::Pressed) {
1905                    return; // Exit early: terminal app handled the input
1906                }
1907
1908                if state == ElementState::Pressed {
1909                    // --- 4. Scrollbar Interaction ---
1910                    // Check if clicking/dragging the scrollbar track or thumb
1911                    let mouse_x = self.mouse_position.0 as f32;
1912                    let mouse_y = self.mouse_position.1 as f32;
1913
1914                    if let Some(renderer) = &self.renderer
1915                        && renderer.scrollbar_track_contains_x(mouse_x)
1916                    {
1917                        self.scroll_state.dragging = true;
1918                        self.scroll_state.last_activity = std::time::Instant::now();
1919
1920                        let thumb_bounds = renderer.scrollbar_thumb_bounds();
1921                        if renderer.scrollbar_contains_point(mouse_x, mouse_y) {
1922                            // Clicked on thumb: track offset from thumb top for precise dragging
1923                            self.scroll_state.drag_offset = thumb_bounds
1924                                .map(|(thumb_top, thumb_height)| {
1925                                    (mouse_y - thumb_top).clamp(0.0, thumb_height)
1926                                })
1927                                .unwrap_or(0.0);
1928                        } else {
1929                            // Clicked on track: center thumb on mouse position
1930                            self.scroll_state.drag_offset = thumb_bounds
1931                                .map(|(_, thumb_height)| thumb_height / 2.0)
1932                                .unwrap_or(0.0);
1933                        }
1934
1935                        self.drag_scrollbar_to(mouse_y);
1936                        return; // Exit early: scrollbar handling takes precedence over selection
1937                    }
1938
1939                    // --- 5. Selection Anchoring & Click Counting ---
1940                    // Handle complex selection modes based on click sequence
1941                    if let Some((col, row)) =
1942                        self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1943                    {
1944                        let now = std::time::Instant::now();
1945                        let same_position = self.click_position == Some((col, row));
1946
1947                        // Thresholds for sequential clicks (double/triple)
1948                        let threshold_ms = if self.click_count == 1 {
1949                            self.config.mouse_double_click_threshold
1950                        } else {
1951                            self.config.mouse_triple_click_threshold
1952                        };
1953                        let click_threshold = std::time::Duration::from_millis(threshold_ms);
1954
1955                        // Increment click counter if within time/space constraints
1956                        if same_position
1957                            && let Some(last_time) = self.last_click_time
1958                            && now.duration_since(last_time) < click_threshold
1959                        {
1960                            self.click_count = (self.click_count + 1).min(3);
1961                        } else {
1962                            self.click_count = 1;
1963                            // Clear previous selection on new single click
1964                            self.selection = None;
1965                        }
1966
1967                        self.last_click_time = Some(now);
1968                        self.click_position = Some((col, row));
1969
1970                        // Apply immediate selection based on click count
1971                        if self.click_count == 2 {
1972                            // Double-click: Anchor word selection
1973                            self.select_word_at(col, row);
1974                            self.is_selecting = false; // Word selection is static until drag starts
1975                            if let Some(window) = &self.window {
1976                                window.request_redraw();
1977                            }
1978                        } else if self.click_count == 3 {
1979                            // Triple-click: Anchor full-line selection
1980                            self.select_line_at(row);
1981                            self.is_selecting = true; // Triple-click usually implies immediate drag intent
1982                            if let Some(window) = &self.window {
1983                                window.request_redraw();
1984                            }
1985                        } else {
1986                            // Single click: Reset state and wait for drag to start Normal/Rectangular selection
1987                            self.is_selecting = false;
1988                            self.selection = None;
1989                            if let Some(window) = &self.window {
1990                                window.request_redraw();
1991                            }
1992                        }
1993                    }
1994                } else {
1995                    // End scrollbar drag
1996                    if self.scroll_state.dragging {
1997                        self.scroll_state.dragging = false;
1998                        self.scroll_state.drag_offset = 0.0;
1999                        return;
2000                    }
2001
2002                    // End selection and optionally copy to clipboard/primary selection
2003                    self.is_selecting = false;
2004
2005                    if let Some(mut selected_text) = self.get_selected_text()
2006                        && !selected_text.is_empty()
2007                    {
2008                        // Strip trailing newline if configured (inverted logic: copy_trailing_newline=false means strip)
2009                        if !self.config.copy_trailing_newline {
2010                            while selected_text.ends_with('\n') || selected_text.ends_with('\r') {
2011                                selected_text.pop();
2012                            }
2013                        }
2014
2015                        // Always copy to primary selection (Linux X11 - no-op on other platforms)
2016                        if let Err(e) = self.input_handler.copy_to_primary_selection(&selected_text)
2017                        {
2018                            log::debug!("Failed to copy to primary selection: {}", e);
2019                        } else {
2020                            log::debug!(
2021                                "Copied {} chars to primary selection",
2022                                selected_text.len()
2023                            );
2024                        }
2025
2026                        // Copy to clipboard if auto_copy is enabled
2027                        if self.config.auto_copy_selection {
2028                            if let Err(e) = self.input_handler.copy_to_clipboard(&selected_text) {
2029                                log::error!("Failed to copy to clipboard: {}", e);
2030                            } else {
2031                                log::debug!("Copied {} chars to clipboard", selected_text.len());
2032                            }
2033                        }
2034
2035                        // Add to clipboard history (once, regardless of which clipboard was used)
2036                        if let Some(terminal) = &self.terminal
2037                            && let Ok(term) = terminal.try_lock()
2038                        {
2039                            term.add_to_clipboard_history(
2040                                ClipboardSlot::Clipboard,
2041                                selected_text.clone(),
2042                                None,
2043                            );
2044                        }
2045                    }
2046                }
2047            }
2048            MouseButton::Middle => {
2049                // Try to send to terminal if mouse tracking is enabled
2050                if self.try_send_mouse_event(1, state == ElementState::Pressed) {
2051                    return; // Event consumed by terminal
2052                }
2053
2054                // Handle middle-click paste if configured
2055                if state == ElementState::Pressed && self.config.middle_click_paste {
2056                    // Paste from primary selection (Linux X11) or clipboard (fallback)
2057                    if let Some(bytes) = self.input_handler.paste_from_primary_selection()
2058                        && let Some(terminal) = &self.terminal
2059                    {
2060                        let terminal_clone = Arc::clone(terminal);
2061                        self.runtime.spawn(async move {
2062                            let term = terminal_clone.lock().await;
2063                            let _ = term.write(&bytes);
2064                        });
2065                    }
2066                }
2067            }
2068            MouseButton::Right => {
2069                // Try to send to terminal if mouse tracking is enabled
2070                let _ = self.try_send_mouse_event(2, state == ElementState::Pressed);
2071                // Event consumed by terminal (or ignored)
2072            }
2073            _ => {}
2074        }
2075    }
2076
2077    fn handle_mouse_move(&mut self, position: (f64, f64)) {
2078        self.mouse_position = position;
2079
2080        // --- 1. Shader Uniform Updates ---
2081        // Update current mouse position for custom shaders (iMouse.xy)
2082        if let Some(ref mut renderer) = self.renderer {
2083            renderer.set_shader_mouse_position(position.0 as f32, position.1 as f32);
2084        }
2085
2086        // --- 2. URL Hover Detection ---
2087        // Identify if mouse is over a clickable link and update window UI (cursor/title)
2088        if let Some((col, row)) = self.pixel_to_cell(position.0, position.1) {
2089            let adjusted_row = row + self.scroll_state.offset;
2090            let url_opt =
2091                url_detection::find_url_at_position(&self.detected_urls, col, adjusted_row);
2092
2093            if let Some(url) = url_opt {
2094                // Hovering over a new/different URL
2095                if self.hovered_url.as_ref() != Some(&url.url) {
2096                    self.hovered_url = Some(url.url.clone());
2097                    if let Some(window) = &self.window {
2098                        // Visual feedback: hand pointer + URL tooltip in title
2099                        window.set_cursor(winit::window::CursorIcon::Pointer);
2100                        let tooltip_title = format!("{} - {}", self.config.window_title, url.url);
2101                        window.set_title(&tooltip_title);
2102                    }
2103                }
2104            } else {
2105                // Mouse left a URL area: restore default state
2106                if self.hovered_url.is_some() {
2107                    self.hovered_url = None;
2108                    if let Some(window) = &self.window {
2109                        window.set_cursor(winit::window::CursorIcon::Text);
2110                        // Restore terminal-controlled title or config default
2111                        if self.config.allow_title_change && !self.cached_terminal_title.is_empty()
2112                        {
2113                            window.set_title(&self.cached_terminal_title);
2114                        } else {
2115                            window.set_title(&self.config.window_title);
2116                        }
2117                    }
2118                }
2119            }
2120        }
2121
2122        // --- 3. Mouse Motion Reporting ---
2123        // Forward motion events to PTY if requested by terminal app (e.g., mouse tracking in vim)
2124        if let Some(terminal) = &self.terminal
2125            && let Some((col, row)) = self.pixel_to_cell(position.0, position.1)
2126            && let Ok(term) = terminal.try_lock()
2127            && term.should_report_mouse_motion(self.mouse_button_pressed)
2128        {
2129            // Encode button+motion (button 32 marker)
2130            let button = if self.mouse_button_pressed {
2131                32 // Motion while button pressed
2132            } else {
2133                35 // Motion without button pressed
2134            };
2135
2136            let encoded = term.encode_mouse_event(button, col, row, true, 0);
2137            if !encoded.is_empty() {
2138                let terminal_clone = Arc::clone(terminal);
2139                let runtime = Arc::clone(&self.runtime);
2140                runtime.spawn(async move {
2141                    let t = terminal_clone.lock().await;
2142                    let _ = t.write(&encoded);
2143                });
2144            }
2145            return; // Exit early: terminal app is handling mouse motion
2146        }
2147
2148        // --- 4. Scrollbar Dragging ---
2149        if self.scroll_state.dragging {
2150            self.scroll_state.last_activity = std::time::Instant::now();
2151            self.drag_scrollbar_to(position.1 as f32);
2152            return; // Exit early: scrollbar dragging takes precedence over selection
2153        }
2154
2155        // --- 5. Drag Selection Logic ---
2156        // Perform local text selection if mouse tracking is NOT active
2157        let alt_screen_active = self
2158            .terminal
2159            .as_ref()
2160            .and_then(|t| t.try_lock().ok())
2161            .is_some_and(|term| term.is_alt_screen_active());
2162
2163        if let Some((col, row)) = self.pixel_to_cell(position.0, position.1)
2164            && self.mouse_button_pressed
2165            && !alt_screen_active
2166        {
2167            if self.click_count == 1 && !self.is_selecting {
2168                // Initial drag move: Start selection if we've moved past the click threshold
2169                if let Some(click_pos) = self.click_position
2170                    && click_pos != (col, row)
2171                {
2172                    self.is_selecting = true;
2173                    // Alt key triggers Rectangular/Block selection mode
2174                    let mode = if self.input_handler.modifiers.state().alt_key() {
2175                        SelectionMode::Rectangular
2176                    } else {
2177                        SelectionMode::Normal
2178                    };
2179                    self.selection = Some(Selection::new(
2180                        self.click_position.unwrap(),
2181                        (col, row),
2182                        mode,
2183                    ));
2184                    if let Some(window) = &self.window {
2185                        window.request_redraw();
2186                    }
2187                }
2188            } else if self.is_selecting {
2189                // Dragging in progress: Update selection endpoints
2190                if let Some(ref selection) = self.selection {
2191                    if selection.mode == SelectionMode::Line {
2192                        // Triple-click mode: Selection always covers whole lines
2193                        self.extend_line_selection(row);
2194                        if let Some(window) = &self.window {
2195                            window.request_redraw();
2196                        }
2197                    } else {
2198                        // Normal/Rectangular mode: update end cell
2199                        if let Some(ref mut sel) = self.selection {
2200                            sel.end = (col, row);
2201                            if let Some(window) = &self.window {
2202                                window.request_redraw();
2203                            }
2204                        }
2205                    }
2206                }
2207            }
2208        }
2209    }
2210
2211    fn drag_scrollbar_to(&mut self, mouse_y: f32) {
2212        if let Some(renderer) = &self.renderer {
2213            let adjusted_y = mouse_y - self.scroll_state.drag_offset;
2214            if let Some(new_offset) = renderer.scrollbar_mouse_y_to_scroll_offset(adjusted_y)
2215                && self.scroll_state.offset != new_offset
2216            {
2217                // Instant update for dragging (no animation)
2218                self.scroll_state.offset = new_offset;
2219                self.scroll_state.target_offset = new_offset;
2220                self.scroll_state.animated_offset = new_offset as f64;
2221                self.scroll_state.animation_start = None;
2222
2223                if let Some(window) = &self.window {
2224                    window.request_redraw();
2225                }
2226            }
2227        }
2228    }
2229
2230    fn render(&mut self) {
2231        // Skip rendering if shutting down
2232        if self.is_shutting_down {
2233            return;
2234        }
2235
2236        let absolute_start = std::time::Instant::now();
2237
2238        // Reset redraw flag after rendering
2239        // This flag will be set again in about_to_wait if another redraw is needed
2240        self.needs_redraw = false;
2241
2242        // Track frame timing
2243        let frame_start = std::time::Instant::now();
2244
2245        // Calculate frame time from last render
2246        if let Some(last_start) = self.debug_last_frame_start {
2247            let frame_time = frame_start.duration_since(last_start);
2248            self.debug_frame_times.push(frame_time);
2249            if self.debug_frame_times.len() > 60 {
2250                self.debug_frame_times.remove(0);
2251            }
2252        }
2253        self.debug_last_frame_start = Some(frame_start);
2254
2255        // Update scroll animation
2256        let animation_running = self.update_scroll_animation();
2257
2258        // Rebuild renderer if font-related settings changed
2259        if self.pending_font_rebuild {
2260            if let Err(e) = self.rebuild_renderer() {
2261                log::error!("Failed to rebuild renderer after font change: {}", e);
2262            }
2263            self.pending_font_rebuild = false;
2264        }
2265
2266        let (renderer_size, visible_lines) = if let Some(renderer) = &self.renderer {
2267            (renderer.size(), renderer.grid_size().1)
2268        } else {
2269            return;
2270        };
2271
2272        let terminal = if let Some(terminal) = &self.terminal {
2273            terminal
2274        } else {
2275            return;
2276        };
2277
2278        // Check if shell has exited
2279        let _is_running = if let Ok(term) = terminal.try_lock() {
2280            term.is_running()
2281        } else {
2282            true // Assume running if locked
2283        };
2284
2285        // Request another redraw if animation is still running
2286        if animation_running && let Some(window) = &self.window {
2287            window.request_redraw();
2288        }
2289
2290        // Get terminal cells for rendering (with dirty tracking optimization)
2291        let (cells, current_cursor_pos, cursor_style) = if let Ok(term) = terminal.try_lock() {
2292            // Get current generation to check if terminal content has changed
2293            let current_generation = term.update_generation();
2294
2295            // Normalize selection if it exists and extract mode
2296            let (selection, rectangular) = if let Some(sel) = self.selection {
2297                (
2298                    Some(sel.normalized()),
2299                    sel.mode == SelectionMode::Rectangular,
2300                )
2301            } else {
2302                (None, false)
2303            };
2304
2305            // Get cursor position and opacity (only show if we're at the bottom with no scroll offset
2306            // and the cursor is visible - TUI apps hide cursor via DECTCEM escape sequence)
2307            let current_cursor_pos = if self.scroll_state.offset == 0 && term.is_cursor_visible() {
2308                Some(term.cursor_position())
2309            } else {
2310                None
2311            };
2312
2313            let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
2314
2315            // Get cursor style for geometric rendering
2316            let cursor_style = if current_cursor_pos.is_some() {
2317                Some(term.cursor_style())
2318            } else {
2319                None
2320            };
2321
2322            log::trace!(
2323                "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
2324                current_cursor_pos,
2325                self.cursor_opacity,
2326                cursor_style,
2327                self.scroll_state.offset,
2328                term.is_cursor_visible()
2329            );
2330
2331            // Check if we need to regenerate cells
2332            // Only regenerate when content actually changes, not on every cursor blink
2333            let needs_regeneration = self.cached_cells.is_none()
2334                || current_generation != self.last_generation
2335                || self.scroll_state.offset != self.last_scroll_offset
2336                || current_cursor_pos != self.last_cursor_pos // Regenerate if cursor position changed
2337                || self.selection != self.last_selection; // Regenerate if selection changed (including clearing)
2338
2339            let cell_gen_start = std::time::Instant::now();
2340            let (cells, is_cache_hit) = if needs_regeneration {
2341                // Generate fresh cells
2342                let fresh_cells = term.get_cells_with_scrollback(
2343                    self.scroll_state.offset,
2344                    selection,
2345                    rectangular,
2346                    cursor,
2347                );
2348
2349                // Update cache
2350                self.cached_cells = Some(fresh_cells.clone());
2351                self.last_generation = current_generation;
2352                self.last_scroll_offset = self.scroll_state.offset;
2353                self.last_cursor_pos = current_cursor_pos;
2354                self.last_selection = self.selection;
2355
2356                (fresh_cells, false)
2357            } else {
2358                // Use cached cells - clone is still needed because of apply_url_underlines
2359                // but we track it accurately for debug logging
2360                (self.cached_cells.as_ref().unwrap().clone(), true)
2361            };
2362            self.debug_cache_hit = is_cache_hit;
2363            self.debug_cell_gen_time = cell_gen_start.elapsed();
2364
2365            (cells, current_cursor_pos, cursor_style)
2366        } else {
2367            return; // Terminal locked, skip this frame
2368        };
2369
2370        // Get scrollback length and terminal title from terminal
2371        // Note: When terminal width changes during resize, the core library clears
2372        // scrollback because the old cells would be misaligned with the new column count.
2373        // This is a limitation of the current implementation - proper reflow is not yet supported.
2374        let (scrollback_len, terminal_title) = if let Ok(term) = terminal.try_lock() {
2375            (term.scrollback_len(), term.get_title())
2376        } else {
2377            (
2378                self.cached_scrollback_len,
2379                self.cached_terminal_title.clone(),
2380            )
2381        };
2382
2383        self.cached_scrollback_len = scrollback_len;
2384        self.scroll_state
2385            .clamp_to_scrollback(self.cached_scrollback_len);
2386
2387        // Update window title if terminal has set one via OSC sequences
2388        // Only if allow_title_change is enabled and we're not showing a URL tooltip
2389        if self.config.allow_title_change
2390            && self.hovered_url.is_none()
2391            && terminal_title != self.cached_terminal_title
2392            && let Some(window) = &self.window
2393        {
2394            self.cached_terminal_title = terminal_title.clone();
2395            if terminal_title.is_empty() {
2396                // Restore configured title when terminal clears title
2397                window.set_title(&self.config.window_title);
2398            } else {
2399                // Use terminal-set title
2400                window.set_title(&terminal_title);
2401            }
2402        }
2403
2404        // Total lines = visible lines + actual scrollback content
2405        let total_lines = visible_lines + scrollback_len;
2406
2407        // Detect URLs in visible area (only when content changed)
2408        // This optimization saves ~0.26ms per frame on cache hits
2409        let url_detect_start = std::time::Instant::now();
2410        let debug_url_detect_time = if !self.debug_cache_hit {
2411            // Content changed - re-detect URLs
2412            self.detect_urls();
2413            url_detect_start.elapsed()
2414        } else {
2415            // Content unchanged - use cached URL detection
2416            std::time::Duration::ZERO
2417        };
2418
2419        // Apply URL underlining to cells (always apply, since cells might be regenerated)
2420        let url_underline_start = std::time::Instant::now();
2421        let mut cells = cells; // Make cells mutable
2422        self.apply_url_underlines(&mut cells, &renderer_size);
2423        let _debug_url_underline_time = url_underline_start.elapsed();
2424
2425        // Update cursor blink state
2426        self.update_cursor_blink();
2427
2428        let render_start = std::time::Instant::now();
2429
2430        let mut debug_update_cells_time = std::time::Duration::ZERO;
2431        #[allow(unused_assignments)]
2432        let mut debug_graphics_time = std::time::Duration::ZERO;
2433        #[allow(unused_assignments)]
2434        let mut debug_actual_render_time = std::time::Duration::ZERO;
2435        let _ = &debug_actual_render_time;
2436        // Clipboard action to handle after rendering (declared here to survive renderer borrow)
2437        let mut pending_clipboard_action = ClipboardHistoryAction::None;
2438
2439        let show_scrollbar = self.should_show_scrollbar();
2440
2441        if let Some(renderer) = &mut self.renderer {
2442            // Only update renderer with cells if they changed (cache MISS)
2443            // This avoids re-uploading the same cell data to GPU on every frame
2444            if !self.debug_cache_hit {
2445                let t = std::time::Instant::now();
2446                renderer.update_cells(&cells);
2447                debug_update_cells_time = t.elapsed();
2448            }
2449
2450            // Update cursor position and style for geometric rendering
2451            if let (Some(pos), Some(opacity), Some(style)) =
2452                (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
2453            {
2454                renderer.update_cursor(pos, opacity, style);
2455                // Forward cursor state to custom shader for Ghostty-compatible cursor animations
2456                // Use the configured cursor color
2457                let cursor_color = [
2458                    self.config.cursor_color[0] as f32 / 255.0,
2459                    self.config.cursor_color[1] as f32 / 255.0,
2460                    self.config.cursor_color[2] as f32 / 255.0,
2461                    1.0,
2462                ];
2463                renderer.update_shader_cursor(pos.0, pos.1, opacity, cursor_color, style);
2464            } else {
2465                renderer.clear_cursor();
2466            }
2467
2468            // If settings UI is visible, sync app config to UI working copy and push opacity
2469            if self.settings_ui.visible {
2470                let ui_cfg = self.settings_ui.current_config().clone();
2471                if (ui_cfg.window_opacity - self.config.window_opacity).abs() > 1e-4 {
2472                    log::info!(
2473                        "Syncing live opacity from UI {:.3} (app {:.3})",
2474                        ui_cfg.window_opacity,
2475                        self.config.window_opacity
2476                    );
2477                    self.config.window_opacity = ui_cfg.window_opacity;
2478                }
2479
2480                renderer.update_opacity(self.config.window_opacity);
2481                self.last_applied_opacity = self.config.window_opacity;
2482                self.cached_cells = None;
2483                if let Some(window) = &self.window {
2484                    window.request_redraw();
2485                }
2486            }
2487
2488            // Update scrollbar
2489            renderer.update_scrollbar(self.scroll_state.offset, visible_lines, total_lines);
2490
2491            // Update animations and request redraw if frames changed
2492            let anim_start = std::time::Instant::now();
2493            if let Some(terminal) = &self.terminal {
2494                let terminal = terminal.blocking_lock();
2495                if terminal.update_animations() {
2496                    // Animation frame changed - request continuous redraws while animations are playing
2497                    if let Some(window) = &self.window {
2498                        window.request_redraw();
2499                    }
2500                }
2501            }
2502            let debug_anim_time = anim_start.elapsed();
2503
2504            // Update graphics from terminal (pass scroll_offset for view adjustment)
2505            // Include both current screen graphics and scrollback graphics
2506            // Use get_graphics_with_animations() to get current animation frames
2507            let graphics_start = std::time::Instant::now();
2508            if let Some(terminal) = &self.terminal {
2509                let terminal = terminal.blocking_lock();
2510                let mut graphics = terminal.get_graphics_with_animations();
2511                let scrollback_len = terminal.scrollback_len();
2512
2513                // Always include scrollback graphics (renderer will calculate visibility)
2514                let scrollback_graphics = terminal.get_scrollback_graphics();
2515                let scrollback_count = scrollback_graphics.len();
2516                graphics.extend(scrollback_graphics);
2517
2518                debug_info!(
2519                    "APP",
2520                    "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
2521                    graphics.len(),
2522                    scrollback_count,
2523                    self.scroll_state.offset,
2524                    scrollback_len
2525                );
2526                if let Err(e) = renderer.update_graphics(
2527                    &graphics,
2528                    self.scroll_state.offset,
2529                    scrollback_len,
2530                    visible_lines,
2531                ) {
2532                    log::error!("Failed to update graphics: {}", e);
2533                }
2534            }
2535            debug_graphics_time = graphics_start.elapsed();
2536
2537            // Calculate visual bell flash intensity (0.0 = no flash, 1.0 = full flash)
2538            let visual_bell_intensity = if let Some(flash_start) = self.visual_bell_flash {
2539                const FLASH_DURATION_MS: u128 = 150;
2540                let elapsed = flash_start.elapsed().as_millis();
2541                if elapsed < FLASH_DURATION_MS {
2542                    // Request continuous redraws while flash is active
2543                    if let Some(window) = &self.window {
2544                        window.request_redraw();
2545                    }
2546                    // Fade out: start at 0.3 intensity, fade to 0
2547                    0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
2548                } else {
2549                    // Flash complete - clear it
2550                    self.visual_bell_flash = None;
2551                    0.0
2552                }
2553            } else {
2554                0.0
2555            };
2556
2557            // Update renderer with visual bell intensity
2558            renderer.set_visual_bell_intensity(visual_bell_intensity);
2559
2560            // Prepare egui output if settings UI is visible
2561            let egui_start = std::time::Instant::now();
2562
2563            // Capture values for FPS overlay before closure
2564            let show_fps = self.show_fps_overlay;
2565            let fps_value = self.fps_value;
2566            let frame_time_ms = if !self.debug_frame_times.is_empty() {
2567                let avg = self.debug_frame_times.iter().sum::<std::time::Duration>()
2568                    / self.debug_frame_times.len() as u32;
2569                avg.as_secs_f64() * 1000.0
2570            } else {
2571                0.0
2572            };
2573
2574            // Track config changes from settings UI (to be applied after egui block)
2575            #[allow(clippy::type_complexity)]
2576            let mut pending_config_update: Option<(
2577                Option<crate::config::Config>,
2578                Option<crate::config::Config>,
2579                Option<ShaderEditorResult>,
2580                Option<CursorShaderEditorResult>,
2581            )> = None;
2582
2583            let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
2584                (&self.egui_ctx, &mut self.egui_state)
2585            {
2586                let raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
2587
2588                let egui_output = egui_ctx.run(raw_input, |ctx| {
2589                    // Show FPS overlay if enabled (top-right corner)
2590                    if show_fps {
2591                        egui::Area::new(egui::Id::new("fps_overlay"))
2592                            .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
2593                            .order(egui::Order::Foreground)
2594                            .show(ctx, |ui| {
2595                                egui::Frame::NONE
2596                                    .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
2597                                    .inner_margin(egui::Margin::same(8))
2598                                    .corner_radius(4.0)
2599                                    .show(ui, |ui| {
2600                                        ui.style_mut().visuals.override_text_color =
2601                                            Some(egui::Color32::from_rgb(0, 255, 0));
2602                                        ui.label(
2603                                            egui::RichText::new(format!(
2604                                                "FPS: {:.1}\nFrame: {:.2}ms",
2605                                                fps_value, frame_time_ms
2606                                            ))
2607                                            .monospace()
2608                                            .size(14.0),
2609                                        );
2610                                    });
2611                            });
2612                    }
2613
2614                    // Show settings UI and store results for later processing
2615                    let settings_result = self.settings_ui.show(ctx);
2616                    pending_config_update = Some(settings_result);
2617
2618                    // Show help UI
2619                    self.help_ui.show(ctx);
2620
2621                    // Show clipboard history UI and collect action
2622                    pending_clipboard_action = self.clipboard_history_ui.show(ctx);
2623                });
2624
2625                // Handle egui platform output (clipboard, cursor changes, etc.)
2626                // This enables cut/copy/paste in egui text editors
2627                egui_state.handle_platform_output(
2628                    self.window.as_ref().unwrap(),
2629                    egui_output.platform_output.clone(),
2630                );
2631
2632                Some((egui_output, egui_ctx))
2633            } else {
2634                None
2635            };
2636
2637            // Process settings changes after egui block (to avoid borrow conflicts)
2638            if let Some((
2639                config_to_save,
2640                config_for_live_update,
2641                shader_apply,
2642                cursor_shader_apply,
2643            )) = pending_config_update
2644            {
2645                // Handle background shader apply request first
2646                if let Some(shader_result) = shader_apply {
2647                    log::info!(
2648                        "Applying background shader from editor ({} bytes)",
2649                        shader_result.source.len()
2650                    );
2651                    match renderer.reload_shader_from_source(&shader_result.source) {
2652                        Ok(()) => {
2653                            log::info!("Background shader applied successfully from editor");
2654                            self.settings_ui.clear_shader_error();
2655                        }
2656                        Err(e) => {
2657                            let error_msg = format!("{:#}", e);
2658                            log::error!("Background shader compilation failed: {}", error_msg);
2659                            self.settings_ui.set_shader_error(Some(error_msg));
2660                        }
2661                    }
2662                }
2663
2664                // Handle cursor shader apply request
2665                if let Some(cursor_shader_result) = cursor_shader_apply {
2666                    log::info!(
2667                        "Applying cursor shader from editor ({} bytes)",
2668                        cursor_shader_result.source.len()
2669                    );
2670                    match renderer.reload_cursor_shader_from_source(&cursor_shader_result.source) {
2671                        Ok(()) => {
2672                            log::info!("Cursor shader applied successfully from editor");
2673                            self.settings_ui.clear_cursor_shader_error();
2674                        }
2675                        Err(e) => {
2676                            let error_msg = format!("{:#}", e);
2677                            log::error!("Cursor shader compilation failed: {}", error_msg);
2678                            self.settings_ui.set_cursor_shader_error(Some(error_msg));
2679                        }
2680                    }
2681                }
2682                // Apply live updates immediately (for visual feedback)
2683                if let Some(live_config) = config_for_live_update {
2684                    let theme_changed = live_config.theme != self.config.theme;
2685                    let shader_animation_changed =
2686                        live_config.custom_shader_animation != self.config.custom_shader_animation;
2687                    let shader_enabled_changed =
2688                        live_config.custom_shader_enabled != self.config.custom_shader_enabled;
2689                    let shader_path_changed =
2690                        live_config.custom_shader != self.config.custom_shader;
2691                    let shader_speed_changed = (live_config.custom_shader_animation_speed
2692                        - self.config.custom_shader_animation_speed)
2693                        .abs()
2694                        > f32::EPSILON;
2695                    let shader_full_content_changed = live_config.custom_shader_full_content
2696                        != self.config.custom_shader_full_content;
2697                    let shader_text_opacity_changed = (live_config.custom_shader_text_opacity
2698                        - self.config.custom_shader_text_opacity)
2699                        .abs()
2700                        > f32::EPSILON;
2701                    let cursor_shader_config_changed = live_config.cursor_shader_color
2702                        != self.config.cursor_shader_color
2703                        || (live_config.cursor_shader_trail_duration
2704                            - self.config.cursor_shader_trail_duration)
2705                            .abs()
2706                            > f32::EPSILON
2707                        || (live_config.cursor_shader_glow_radius
2708                            - self.config.cursor_shader_glow_radius)
2709                            .abs()
2710                            > f32::EPSILON
2711                        || (live_config.cursor_shader_glow_intensity
2712                            - self.config.cursor_shader_glow_intensity)
2713                            .abs()
2714                            > f32::EPSILON;
2715                    let cursor_shader_path_changed =
2716                        live_config.cursor_shader != self.config.cursor_shader;
2717                    let cursor_shader_enabled_changed =
2718                        live_config.cursor_shader_enabled != self.config.cursor_shader_enabled;
2719                    let cursor_shader_animation_changed =
2720                        live_config.cursor_shader_animation != self.config.cursor_shader_animation;
2721                    let cursor_shader_speed_changed = (live_config.cursor_shader_animation_speed
2722                        - self.config.cursor_shader_animation_speed)
2723                        .abs()
2724                        > f32::EPSILON;
2725                    let _scrollbar_position_changed =
2726                        live_config.scrollbar_position != self.config.scrollbar_position;
2727                    let window_title_changed = live_config.window_title != self.config.window_title;
2728                    let window_decorations_changed =
2729                        live_config.window_decorations != self.config.window_decorations;
2730                    let max_fps_changed = live_config.max_fps != self.config.max_fps;
2731                    let cursor_style_changed = live_config.cursor_style != self.config.cursor_style;
2732                    let cursor_color_changed =
2733                        live_config.cursor_color != self.config.cursor_color;
2734                    let bg_enabled_changed = live_config.background_image_enabled
2735                        != self.config.background_image_enabled;
2736                    let bg_path_changed =
2737                        live_config.background_image != self.config.background_image;
2738                    let bg_mode_changed =
2739                        live_config.background_image_mode != self.config.background_image_mode;
2740                    let bg_opacity_changed = (live_config.background_image_opacity
2741                        - self.config.background_image_opacity)
2742                        .abs()
2743                        > f32::EPSILON;
2744                    let font_changed = live_config.font_family != self.config.font_family
2745                        || live_config.font_family_bold != self.config.font_family_bold
2746                        || live_config.font_family_italic != self.config.font_family_italic
2747                        || live_config.font_family_bold_italic
2748                            != self.config.font_family_bold_italic
2749                        || (live_config.font_size - self.config.font_size).abs() > f32::EPSILON
2750                        || (live_config.line_spacing - self.config.line_spacing).abs()
2751                            > f32::EPSILON
2752                        || (live_config.char_spacing - self.config.char_spacing).abs()
2753                            > f32::EPSILON;
2754                    let padding_changed = (live_config.window_padding - self.config.window_padding)
2755                        .abs()
2756                        > f32::EPSILON;
2757                    log::info!(
2758                        "Applying live config update - opacity: {}{}{}",
2759                        live_config.window_opacity,
2760                        if theme_changed {
2761                            " (theme changed)"
2762                        } else {
2763                            ""
2764                        },
2765                        if font_changed { " (font changed)" } else { "" }
2766                    );
2767                    self.config = live_config;
2768                    self.scroll_state.last_activity = std::time::Instant::now();
2769
2770                    // Apply settings that can be updated in real-time
2771                    if let Some(window) = &self.window {
2772                        // Update window level (always on top)
2773                        window.set_window_level(if self.config.window_always_on_top {
2774                            winit::window::WindowLevel::AlwaysOnTop
2775                        } else {
2776                            winit::window::WindowLevel::Normal
2777                        });
2778
2779                        // Update window title
2780                        if window_title_changed {
2781                            window.set_title(&self.config.window_title);
2782                            log::info!("Updated window title to: {}", self.config.window_title);
2783                        }
2784
2785                        // Update window decorations
2786                        if window_decorations_changed {
2787                            window.set_decorations(self.config.window_decorations);
2788                            log::info!(
2789                                "Updated window decorations: {}",
2790                                self.config.window_decorations
2791                            );
2792                        }
2793
2794                        // Request redraw to apply visual changes
2795                        window.request_redraw();
2796                    }
2797
2798                    // Update max_fps (restart refresh timer with new interval)
2799                    if max_fps_changed {
2800                        // Abort the old timer task
2801                        if let Some(old_task) = self.refresh_task.take() {
2802                            old_task.abort();
2803                        }
2804                        // Start new timer with updated interval
2805                        if let Some(window) = &self.window {
2806                            let window_clone = Arc::clone(window);
2807                            let refresh_interval_ms = 1000 / self.config.max_fps.max(1); // Convert Hz to ms
2808                            let handle = self.runtime.spawn(async move {
2809                                let mut interval = tokio::time::interval(
2810                                    tokio::time::Duration::from_millis(refresh_interval_ms as u64),
2811                                );
2812                                loop {
2813                                    interval.tick().await;
2814                                    window_clone.request_redraw();
2815                                }
2816                            });
2817                            self.refresh_task = Some(handle);
2818                            log::info!(
2819                                "Updated max_fps to {} ({}ms interval)",
2820                                self.config.max_fps,
2821                                refresh_interval_ms
2822                            );
2823                        }
2824                    }
2825
2826                    // Update renderer with real-time settings
2827                    renderer.update_opacity(self.config.window_opacity);
2828                    renderer.update_scrollbar_appearance(
2829                        self.config.scrollbar_width,
2830                        self.config.scrollbar_thumb_color,
2831                        self.config.scrollbar_track_color,
2832                    );
2833                    // Scrollbar position is now fixed to right; ignore config changes
2834
2835                    if cursor_style_changed {
2836                        // Set cursor style directly on the terminal (no need to send DECSCUSR to PTY)
2837                        // This updates the terminal's internal cursor state without involving the shell
2838                        if let Some(terminal_mgr) = &self.terminal
2839                            && let Ok(term_mgr) = terminal_mgr.try_lock()
2840                        {
2841                            // Get the underlying Terminal from TerminalManager
2842                            let terminal = term_mgr.terminal();
2843                            if let Some(mut term) = terminal.try_lock() {
2844                                // Convert config cursor style to terminal cursor style
2845                                use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
2846                                let term_style = match self.config.cursor_style {
2847                                    crate::config::CursorStyle::Block => {
2848                                        TermCursorStyle::SteadyBlock
2849                                    }
2850                                    crate::config::CursorStyle::Underline => {
2851                                        TermCursorStyle::SteadyUnderline
2852                                    }
2853                                    crate::config::CursorStyle::Beam => TermCursorStyle::SteadyBar,
2854                                };
2855                                term.set_cursor_style(term_style);
2856                            }
2857                        }
2858
2859                        // Force cell regen to reflect cursor style change
2860                        self.cached_cells = None;
2861                        self.last_cursor_pos = None;
2862                        if let Some(window) = &self.window {
2863                            window.request_redraw();
2864                        }
2865                    }
2866
2867                    // Update cursor color
2868                    if cursor_color_changed {
2869                        renderer.update_cursor_color(self.config.cursor_color);
2870                        // Force cell regen to reflect cursor color change
2871                        self.cached_cells = None;
2872                        self.last_cursor_pos = None;
2873                        if let Some(window) = &self.window {
2874                            window.request_redraw();
2875                        }
2876                    }
2877
2878                    if self.config.background_image_enabled {
2879                        renderer
2880                            .update_background_image_opacity(self.config.background_image_opacity);
2881                    }
2882
2883                    if bg_enabled_changed
2884                        || bg_path_changed
2885                        || bg_mode_changed
2886                        || bg_opacity_changed
2887                    {
2888                        renderer.set_background_image_enabled(
2889                            self.config.background_image_enabled,
2890                            self.config.background_image.as_deref(),
2891                            self.config.background_image_mode,
2892                            self.config.background_image_opacity,
2893                        );
2894                    }
2895
2896                    if shader_animation_changed
2897                        || shader_enabled_changed
2898                        || shader_path_changed
2899                        || shader_speed_changed
2900                        || shader_full_content_changed
2901                        || shader_text_opacity_changed
2902                    {
2903                        match renderer.set_custom_shader_enabled(
2904                            self.config.custom_shader_enabled,
2905                            self.config.custom_shader.as_deref(),
2906                            self.config.window_opacity,
2907                            self.config.custom_shader_text_opacity,
2908                            self.config.custom_shader_animation,
2909                            self.config.custom_shader_animation_speed,
2910                            self.config.custom_shader_full_content,
2911                        ) {
2912                            Ok(()) => {
2913                                self.settings_ui.clear_shader_error();
2914                            }
2915                            Err(error_msg) => {
2916                                log::error!("Shader compilation failed: {}", error_msg);
2917                                self.settings_ui.set_shader_error(Some(error_msg));
2918                            }
2919                        }
2920                    }
2921
2922                    // Update cursor shader configuration
2923                    if cursor_shader_config_changed {
2924                        renderer.update_cursor_shader_config(
2925                            self.config.cursor_shader_color,
2926                            self.config.cursor_shader_trail_duration,
2927                            self.config.cursor_shader_glow_radius,
2928                            self.config.cursor_shader_glow_intensity,
2929                        );
2930                    }
2931
2932                    // Handle cursor shader path/enabled/animation changes
2933                    if cursor_shader_path_changed
2934                        || cursor_shader_enabled_changed
2935                        || cursor_shader_animation_changed
2936                        || cursor_shader_speed_changed
2937                    {
2938                        match renderer.set_cursor_shader_enabled(
2939                            self.config.cursor_shader_enabled,
2940                            self.config.cursor_shader.as_deref(),
2941                            self.config.window_opacity,
2942                            self.config.cursor_shader_animation,
2943                            self.config.cursor_shader_animation_speed,
2944                        ) {
2945                            Ok(()) => {
2946                                self.settings_ui.clear_cursor_shader_error();
2947                            }
2948                            Err(error_msg) => {
2949                                log::error!("Cursor shader compilation failed: {}", error_msg);
2950                                self.settings_ui.set_cursor_shader_error(Some(error_msg));
2951                            }
2952                        }
2953                    }
2954
2955                    // Apply theme changes immediately to the terminal
2956                    if theme_changed {
2957                        if let Some(terminal) = &self.terminal
2958                            && let Ok(mut term) = terminal.try_lock()
2959                        {
2960                            term.set_theme(self.config.load_theme());
2961                            log::info!("Applied live theme change: {}", self.config.theme);
2962                        }
2963                        // Force redraw so new theme colors show up right away
2964                        self.cached_cells = None;
2965                        if let Some(window) = &self.window {
2966                            window.request_redraw();
2967                        }
2968                    }
2969
2970                    if font_changed {
2971                        // Rebuild renderer on next frame to apply font/spacing changes without restart
2972                        self.pending_font_rebuild = true;
2973                        log::info!("Queued renderer rebuild for font change");
2974                    }
2975
2976                    // Update window padding dynamically (no rebuild needed)
2977                    if padding_changed {
2978                        if let Some((cols, rows)) =
2979                            renderer.update_window_padding(self.config.window_padding)
2980                        {
2981                            // Grid size changed - resize terminal to match
2982                            let cell_width = renderer.cell_width();
2983                            let cell_height = renderer.cell_height();
2984                            let width_px = (cols as f32 * cell_width) as usize;
2985                            let height_px = (rows as f32 * cell_height) as usize;
2986
2987                            if let Some(terminal) = &self.terminal
2988                                && let Ok(mut term) = terminal.try_lock()
2989                            {
2990                                let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
2991                                log::info!(
2992                                    "Resized terminal to {}x{} due to padding change",
2993                                    cols,
2994                                    rows
2995                                );
2996                            }
2997                        }
2998                        log::info!("Updated window padding to {}", self.config.window_padding);
2999                    }
3000
3001                    // Invalidate cell cache to force regeneration with new opacity
3002                    self.cached_cells = None;
3003
3004                    // Track last applied opacity
3005                    self.last_applied_opacity = self.config.window_opacity;
3006
3007                    // Request multiple redraws to ensure changes are visible
3008                    if let Some(window) = &self.window {
3009                        window.request_redraw();
3010                    }
3011                }
3012
3013                // Persist to disk if save was clicked
3014                if let Some(new_config) = config_to_save {
3015                    if let Err(e) = new_config.save() {
3016                        log::error!("Failed to save config: {}", e);
3017                    } else {
3018                        log::info!("Configuration saved successfully");
3019                        log::info!(
3020                            "  Bell settings: sound={}, visual={}, desktop={}",
3021                            new_config.notification_bell_sound,
3022                            new_config.notification_bell_visual,
3023                            new_config.notification_bell_desktop
3024                        );
3025                        // Update settings_ui with saved config
3026                        self.settings_ui.update_config(new_config);
3027                    }
3028                }
3029            }
3030            let debug_egui_time = egui_start.elapsed();
3031
3032            // Calculate FPS and timing stats
3033            let avg_frame_time = if !self.debug_frame_times.is_empty() {
3034                self.debug_frame_times.iter().sum::<std::time::Duration>()
3035                    / self.debug_frame_times.len() as u32
3036            } else {
3037                std::time::Duration::ZERO
3038            };
3039            let fps = if avg_frame_time.as_secs_f64() > 0.0 {
3040                1.0 / avg_frame_time.as_secs_f64()
3041            } else {
3042                0.0
3043            };
3044
3045            // Update FPS value for overlay display
3046            self.fps_value = fps;
3047
3048            // Log debug info every 60 frames (about once per second at 60 FPS)
3049            if self.debug_frame_times.len() >= 60 {
3050                log::info!(
3051                    "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={}",
3052                    fps,
3053                    avg_frame_time.as_secs_f64() * 1000.0,
3054                    self.debug_cell_gen_time.as_secs_f64() * 1000.0,
3055                    if self.debug_cache_hit { "HIT" } else { "MISS" },
3056                    debug_url_detect_time.as_secs_f64() * 1000.0,
3057                    debug_anim_time.as_secs_f64() * 1000.0,
3058                    debug_graphics_time.as_secs_f64() * 1000.0,
3059                    debug_egui_time.as_secs_f64() * 1000.0,
3060                    debug_update_cells_time.as_secs_f64() * 1000.0,
3061                    debug_actual_render_time.as_secs_f64() * 1000.0,
3062                    self.debug_render_time.as_secs_f64() * 1000.0,
3063                    cells.len(),
3064                    self.last_generation,
3065                    if self.cached_cells.is_some() {
3066                        "YES"
3067                    } else {
3068                        "NO"
3069                    }
3070                );
3071            }
3072
3073            // Render (with dirty tracking optimization)
3074            let actual_render_start = std::time::Instant::now();
3075            match renderer.render(egui_data, self.settings_ui.visible, show_scrollbar) {
3076                Ok(rendered) => {
3077                    if !rendered {
3078                        log::trace!("Skipped rendering - no changes");
3079                    }
3080                }
3081                Err(e) => {
3082                    // Check if this is a wgpu surface error that requires reconfiguration
3083                    // This commonly happens when dragging windows between displays
3084                    if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
3085                        match surface_error {
3086                            SurfaceError::Outdated | SurfaceError::Lost => {
3087                                log::warn!(
3088                                    "Surface error detected ({:?}), reconfiguring...",
3089                                    surface_error
3090                                );
3091                                self.force_surface_reconfigure();
3092                            }
3093                            SurfaceError::Timeout => {
3094                                log::warn!("Surface timeout, will retry next frame");
3095                                if let Some(window) = &self.window {
3096                                    window.request_redraw();
3097                                }
3098                            }
3099                            SurfaceError::OutOfMemory => {
3100                                log::error!("Surface out of memory: {:?}", surface_error);
3101                            }
3102                            _ => {
3103                                log::error!("Surface error: {:?}", surface_error);
3104                            }
3105                        }
3106                    } else {
3107                        log::error!("Render error: {}", e);
3108                    }
3109                }
3110            }
3111            debug_actual_render_time = actual_render_start.elapsed();
3112            let _ = debug_actual_render_time;
3113
3114            self.debug_render_time = render_start.elapsed();
3115        }
3116
3117        // Handle clipboard actions collected during egui rendering
3118        // (done here to avoid borrow conflicts with renderer)
3119        match pending_clipboard_action {
3120            ClipboardHistoryAction::Paste(content) => {
3121                self.paste_text(&content);
3122            }
3123            ClipboardHistoryAction::ClearAll => {
3124                if let Some(terminal) = &self.terminal
3125                    && let Ok(term) = terminal.try_lock()
3126                {
3127                    term.clear_all_clipboard_history();
3128                    log::info!("Cleared all clipboard history");
3129                }
3130                self.clipboard_history_ui.update_entries(Vec::new());
3131            }
3132            ClipboardHistoryAction::ClearSlot(slot) => {
3133                if let Some(terminal) = &self.terminal
3134                    && let Ok(term) = terminal.try_lock()
3135                {
3136                    term.clear_clipboard_history(slot);
3137                    log::info!("Cleared clipboard history for slot {:?}", slot);
3138                }
3139            }
3140            ClipboardHistoryAction::None => {}
3141        }
3142
3143        let absolute_total = absolute_start.elapsed();
3144        if absolute_total.as_millis() > 10 {
3145            log::debug!(
3146                "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
3147                absolute_total.as_secs_f64() * 1000.0
3148            );
3149        }
3150    }
3151
3152    fn check_notifications(&mut self) {
3153        if let Some(terminal) = &self.terminal
3154            && let Ok(term) = terminal.try_lock()
3155        {
3156            // Check for OSC 9/777 notifications
3157            if term.has_notifications() {
3158                let notifications = term.take_notifications();
3159                for notif in notifications {
3160                    self.deliver_notification(&notif.title, &notif.message);
3161                }
3162            }
3163        }
3164    }
3165
3166    fn check_bell(&mut self) {
3167        // Skip if all bell notifications are disabled
3168        if self.config.notification_bell_sound == 0
3169            && !self.config.notification_bell_visual
3170            && !self.config.notification_bell_desktop
3171        {
3172            return;
3173        }
3174
3175        if let Some(terminal) = &self.terminal
3176            && let Ok(term) = terminal.try_lock()
3177        {
3178            let current_bell_count = term.bell_count();
3179
3180            if current_bell_count > self.last_bell_count {
3181                // Bell event(s) occurred
3182                let bell_events = current_bell_count - self.last_bell_count;
3183                log::info!("🔔 Bell event detected ({} bell(s))", bell_events);
3184                log::info!(
3185                    "  Config: sound={}, visual={}, desktop={}",
3186                    self.config.notification_bell_sound,
3187                    self.config.notification_bell_visual,
3188                    self.config.notification_bell_desktop
3189                );
3190
3191                // Play audio bell if enabled (volume > 0)
3192                if self.config.notification_bell_sound > 0 {
3193                    if let Some(audio_bell) = &self.audio_bell {
3194                        log::info!(
3195                            "  Playing audio bell at {}% volume",
3196                            self.config.notification_bell_sound
3197                        );
3198                        audio_bell.play(self.config.notification_bell_sound);
3199                    } else {
3200                        log::warn!("  Audio bell requested but not initialized");
3201                    }
3202                } else {
3203                    log::debug!("  Audio bell disabled (volume=0)");
3204                }
3205
3206                // Trigger visual bell flash if enabled
3207                if self.config.notification_bell_visual {
3208                    log::info!("  Triggering visual bell flash");
3209                    self.visual_bell_flash = Some(std::time::Instant::now());
3210                    // Request immediate redraw to show flash
3211                    if let Some(window) = &self.window {
3212                        window.request_redraw();
3213                    }
3214                } else {
3215                    log::debug!("  Visual bell disabled");
3216                }
3217
3218                // Send desktop notification if enabled
3219                if self.config.notification_bell_desktop {
3220                    log::info!("  Sending desktop notification");
3221                    let message = if bell_events == 1 {
3222                        "Terminal bell".to_string()
3223                    } else {
3224                        format!("Terminal bell ({} events)", bell_events)
3225                    };
3226                    self.deliver_notification("Terminal", &message);
3227                } else {
3228                    log::debug!("  Desktop notification disabled");
3229                }
3230
3231                self.last_bell_count = current_bell_count;
3232            }
3233        }
3234    }
3235
3236    fn take_screenshot(&self) {
3237        log::info!("Taking screenshot...");
3238
3239        if let Some(terminal) = &self.terminal {
3240            // Generate timestamp-based filename
3241            let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
3242            let format = &self.config.screenshot_format;
3243            let filename = format!("par-term_screenshot_{}.{}", timestamp, format);
3244
3245            // Create screenshots directory in user's home dir
3246            if let Some(home_dir) = dirs::home_dir() {
3247                let screenshot_dir = home_dir.join("par-term-screenshots");
3248                if !screenshot_dir.exists()
3249                    && let Err(e) = std::fs::create_dir_all(&screenshot_dir)
3250                {
3251                    log::error!("Failed to create screenshot directory: {}", e);
3252                    self.deliver_notification(
3253                        "Screenshot Error",
3254                        &format!("Failed to create directory: {}", e),
3255                    );
3256                    return;
3257                }
3258
3259                let path = screenshot_dir.join(&filename);
3260                let path_str = path.to_string_lossy().to_string();
3261
3262                // Take screenshot (include scrollback for better context)
3263                let terminal_clone = Arc::clone(terminal);
3264                let format_clone = format.clone();
3265
3266                // Use async to avoid blocking the UI
3267                let result = std::thread::spawn(move || {
3268                    if let Ok(term) = terminal_clone.try_lock() {
3269                        // Include 0 scrollback lines (just visible content)
3270                        term.screenshot_to_file(&path, &format_clone, 0)
3271                    } else {
3272                        Err(anyhow::anyhow!("Failed to lock terminal"))
3273                    }
3274                })
3275                .join();
3276
3277                match result {
3278                    Ok(Ok(())) => {
3279                        log::info!("Screenshot saved to: {}", path_str);
3280                        self.deliver_notification(
3281                            "Screenshot Saved",
3282                            &format!("Saved to: {}", path_str),
3283                        );
3284                    }
3285                    Ok(Err(e)) => {
3286                        log::error!("Failed to save screenshot: {}", e);
3287                        self.deliver_notification(
3288                            "Screenshot Error",
3289                            &format!("Failed to save: {}", e),
3290                        );
3291                    }
3292                    Err(e) => {
3293                        log::error!("Screenshot thread panicked: {:?}", e);
3294                        self.deliver_notification("Screenshot Error", "Screenshot thread failed");
3295                    }
3296                }
3297            } else {
3298                log::error!("Failed to get home directory");
3299                self.deliver_notification("Screenshot Error", "Failed to get home directory");
3300            }
3301        } else {
3302            log::warn!("No terminal available for screenshot");
3303            self.deliver_notification("Screenshot Error", "No terminal available");
3304        }
3305    }
3306
3307    // TODO: Recording APIs not yet available in par-term-emu-core-rust
3308    // Uncomment when the core library supports recording again
3309    fn toggle_recording(&mut self) {
3310        log::warn!("Recording functionality not yet available in core library");
3311        self.deliver_notification(
3312            "Recording Not Available",
3313            "Recording APIs are not yet implemented in the core library",
3314        );
3315    }
3316
3317    /*
3318    fn toggle_recording(&mut self) {
3319        if self.is_recording {
3320            // Stop recording and save
3321            self.stop_and_save_recording();
3322        } else {
3323            // Start recording
3324            self.start_recording();
3325        }
3326    }
3327
3328    fn start_recording(&mut self) {
3329        log::info!("Starting terminal session recording");
3330
3331        if let Some(terminal) = &self.terminal {
3332            if let Ok(term) = terminal.try_lock() {
3333                // Start recording (no title for now)
3334                term.start_recording(None);
3335
3336                self.is_recording = true;
3337                self.recording_start_time = Some(std::time::Instant::now());
3338                log::info!("Recording started successfully");
3339                self.deliver_notification(
3340                    "Recording Started",
3341                    "Terminal session recording started",
3342                );
3343
3344                // Update window title to show recording status
3345                if let Some(window) = &self.window {
3346                    let title = format!("{} [RECORDING]", self.config.window_title);
3347                    window.set_title(&title);
3348                }
3349            } else {
3350                log::error!("Failed to lock terminal");
3351                self.deliver_notification("Recording Error", "Terminal is busy");
3352            }
3353        } else {
3354            log::warn!("No terminal available for recording");
3355            self.deliver_notification("Recording Error", "No terminal available");
3356        }
3357    }
3358
3359    fn stop_and_save_recording(&mut self) {
3360        log::info!("Stopping terminal session recording");
3361
3362        if let Some(terminal) = &self.terminal {
3363            if let Ok(term) = terminal.try_lock() {
3364                // Stop recording and get the session
3365                let session_opt = term.stop_recording();
3366
3367                if let Some(session) = session_opt {
3368                    // Generate timestamp-based filename
3369                    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
3370                    let filename = format!("par-term_recording_{}.cast", timestamp);
3371
3372                    // Create recordings directory in user's home dir
3373                    if let Some(home_dir) = dirs::home_dir() {
3374                        let recording_dir = home_dir.join("par-term-recordings");
3375                        if let Err(e) = std::fs::create_dir_all(&recording_dir) {
3376                            log::error!("Failed to create recording directory: {}", e);
3377                            self.deliver_notification(
3378                                "Recording Error",
3379                                &format!("Failed to create directory: {}", e),
3380                            );
3381                            self.is_recording = false;
3382                            self.recording_start_time = None;
3383                            return;
3384                        }
3385
3386                        let path = recording_dir.join(&filename);
3387                        let path_str = path.to_string_lossy().to_string();
3388
3389                        // Export to asciicast format and write to file
3390                        match term.export_recording_to_file(&session, &path, "asciicast") {
3391                            Ok(()) => {
3392                                self.is_recording = false;
3393                                let duration = self
3394                                    .recording_start_time
3395                                    .map(|start| start.elapsed().as_secs())
3396                                    .unwrap_or(0);
3397                                self.recording_start_time = None;
3398
3399                                log::info!(
3400                                    "Recording saved to: {} ({} seconds)",
3401                                    path_str,
3402                                    duration
3403                                );
3404                                self.deliver_notification(
3405                                    "Recording Saved",
3406                                    &format!(
3407                                        "Saved to: {}\nDuration: {} seconds",
3408                                        path_str, duration
3409                                    ),
3410                                );
3411
3412                                // Restore window title
3413                                if let Some(window) = &self.window {
3414                                    window.set_title(&self.config.window_title);
3415                                }
3416                            }
3417                            Err(e) => {
3418                                log::error!("Failed to save recording: {}", e);
3419                                self.deliver_notification(
3420                                    "Recording Error",
3421                                    &format!("Failed to save: {}", e),
3422                                );
3423                                self.is_recording = false;
3424                                self.recording_start_time = None;
3425                            }
3426                        }
3427                    } else {
3428                        log::error!("Failed to get home directory");
3429                        self.deliver_notification(
3430                            "Recording Error",
3431                            "Failed to get home directory",
3432                        );
3433                        self.is_recording = false;
3434                        self.recording_start_time = None;
3435                    }
3436                } else {
3437                    log::warn!("No recording session available (recording was not active)");
3438                    self.deliver_notification("Recording Error", "No active recording to save");
3439                    self.is_recording = false;
3440                    self.recording_start_time = None;
3441
3442                    // Restore window title
3443                    if let Some(window) = &self.window {
3444                        window.set_title(&self.config.window_title);
3445                    }
3446                }
3447            } else {
3448                log::error!("Failed to lock terminal");
3449                self.deliver_notification("Recording Error", "Terminal is busy");
3450                self.is_recording = false;
3451                self.recording_start_time = None;
3452            }
3453        } else {
3454            log::warn!("No terminal available");
3455            self.deliver_notification("Recording Error", "No terminal available");
3456            self.is_recording = false;
3457            self.recording_start_time = None;
3458        }
3459    }
3460    */
3461
3462    fn deliver_notification(&self, title: &str, message: &str) {
3463        // Always log notifications
3464        if !title.is_empty() {
3465            log::info!("=== Notification: {} ===", title);
3466            log::info!("{}", message);
3467            log::info!("===========================");
3468        } else {
3469            log::info!("=== Notification ===");
3470            log::info!("{}", message);
3471            log::info!("===================");
3472        }
3473
3474        // Send desktop notification if enabled
3475        #[cfg(not(target_os = "macos"))]
3476        {
3477            use notify_rust::Notification;
3478            let notification_title = if !title.is_empty() {
3479                title
3480            } else {
3481                "Terminal Notification"
3482            };
3483
3484            if let Err(e) = Notification::new()
3485                .summary(notification_title)
3486                .body(message)
3487                .timeout(notify_rust::Timeout::Milliseconds(3000))
3488                .show()
3489            {
3490                log::warn!("Failed to send desktop notification: {}", e);
3491            }
3492        }
3493
3494        #[cfg(target_os = "macos")]
3495        {
3496            // macOS notifications via osascript
3497            let notification_title = if !title.is_empty() {
3498                title
3499            } else {
3500                "Terminal Notification"
3501            };
3502
3503            // Escape quotes in title and message for AppleScript
3504            let escaped_title = notification_title.replace('"', "\\\"");
3505            let escaped_message = message.replace('"', "\\\"");
3506
3507            // Use osascript to display notification
3508            let script = format!(
3509                r#"display notification "{}" with title "{}""#,
3510                escaped_message, escaped_title
3511            );
3512
3513            if let Err(e) = std::process::Command::new("osascript")
3514                .arg("-e")
3515                .arg(&script)
3516                .output()
3517            {
3518                log::warn!("Failed to send macOS desktop notification: {}", e);
3519            }
3520        }
3521    }
3522
3523    /// Update window title with shell integration info (cwd and exit code)
3524    /// Only updates if not scrolled and not hovering over URL
3525    fn update_window_title_with_shell_integration(&self) {
3526        // Skip if scrolled (scrollback indicator takes priority)
3527        if self.scroll_state.offset != 0 {
3528            return;
3529        }
3530
3531        // Skip if hovering over URL (URL tooltip takes priority)
3532        if self.hovered_url.is_some() {
3533            return;
3534        }
3535
3536        // Skip if window not available
3537        let window = if let Some(w) = &self.window {
3538            w
3539        } else {
3540            return;
3541        };
3542
3543        // Skip if terminal not available
3544        let terminal = if let Some(t) = &self.terminal {
3545            t
3546        } else {
3547            return;
3548        };
3549
3550        // Try to get shell integration info
3551        if let Ok(term) = terminal.try_lock() {
3552            let mut title_parts = vec![self.config.window_title.clone()];
3553
3554            // Add current working directory if available
3555            if let Some(cwd) = term.shell_integration_cwd() {
3556                // Abbreviate home directory to ~
3557                let abbreviated_cwd = if let Some(home) = dirs::home_dir() {
3558                    cwd.replace(&home.to_string_lossy().to_string(), "~")
3559                } else {
3560                    cwd
3561                };
3562                title_parts.push(format!("({})", abbreviated_cwd));
3563            }
3564
3565            // Add exit code indicator if last command failed
3566            if let Some(exit_code) = term.shell_integration_exit_code()
3567                && exit_code != 0
3568            {
3569                title_parts.push(format!("[Exit: {}]", exit_code));
3570            }
3571
3572            // Add recording indicator
3573            if self.is_recording {
3574                title_parts.push("[RECORDING]".to_string());
3575            }
3576
3577            // Build and set title
3578            let title = title_parts.join(" ");
3579            window.set_title(&title);
3580        }
3581    }
3582}
3583
3584impl ApplicationHandler for AppState {
3585    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
3586        if self.window.is_none() {
3587            let mut window_attrs = Window::default_attributes()
3588                .with_title(&self.config.window_title)
3589                .with_inner_size(winit::dpi::LogicalSize::new(
3590                    self.config.window_width,
3591                    self.config.window_height,
3592                ))
3593                .with_decorations(self.config.window_decorations);
3594
3595            // Load and set the application icon
3596            let icon_bytes = include_bytes!("../assets/icon.png");
3597            if let Ok(icon_image) = image::load_from_memory(icon_bytes) {
3598                let rgba = icon_image.to_rgba8();
3599                let (width, height) = rgba.dimensions();
3600                if let Ok(icon) = winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
3601                    window_attrs = window_attrs.with_window_icon(Some(icon));
3602                    log::info!("Window icon set ({}x{})", width, height);
3603                } else {
3604                    log::warn!("Failed to create window icon from RGBA data");
3605                }
3606            } else {
3607                log::warn!("Failed to load embedded icon image");
3608            }
3609
3610            // Set window always-on-top if requested
3611            if self.config.window_always_on_top {
3612                window_attrs =
3613                    window_attrs.with_window_level(winit::window::WindowLevel::AlwaysOnTop);
3614                log::info!("Window always-on-top enabled");
3615            }
3616
3617            // Always enable window transparency support for runtime opacity changes
3618            // Even if starting at opacity 1.0, we need this for real-time updates
3619            window_attrs = window_attrs.with_transparent(true);
3620            log::info!(
3621                "Window transparency enabled (opacity: {})",
3622                self.config.window_opacity
3623            );
3624
3625            match event_loop.create_window(window_attrs) {
3626                Ok(window) => {
3627                    // Initialize async components using the shared runtime
3628                    let runtime = Arc::clone(&self.runtime);
3629                    if let Err(e) = runtime.block_on(self.initialize_async(window)) {
3630                        log::error!("Failed to initialize: {}", e);
3631                        event_loop.exit();
3632                    }
3633                }
3634                Err(e) => {
3635                    log::error!("Failed to create window: {}", e);
3636                    event_loop.exit();
3637                }
3638            }
3639        }
3640    }
3641
3642    fn window_event(
3643        &mut self,
3644        event_loop: &ActiveEventLoop,
3645        _window_id: WindowId,
3646        event: WindowEvent,
3647    ) {
3648        use winit::keyboard::{Key, NamedKey};
3649
3650        // Debug: Log ALL keyboard events at entry to diagnose Space issue
3651        if let WindowEvent::KeyboardInput {
3652            event: key_event, ..
3653        } = &event
3654        {
3655            match &key_event.logical_key {
3656                Key::Character(s) => {
3657                    log::trace!(
3658                        "window_event: Character '{}', state={:?}",
3659                        s,
3660                        key_event.state
3661                    );
3662                }
3663                Key::Named(NamedKey::Space) => {
3664                    log::debug!("🔔 SPACE EVENT: state={:?}", key_event.state);
3665                }
3666                Key::Named(named) => {
3667                    log::trace!(
3668                        "window_event: Named key {:?}, state={:?}",
3669                        named,
3670                        key_event.state
3671                    );
3672                }
3673                other => {
3674                    log::trace!(
3675                        "window_event: Other key {:?}, state={:?}",
3676                        other,
3677                        key_event.state
3678                    );
3679                }
3680            }
3681        }
3682
3683        // Let egui handle the event (needed for proper rendering state)
3684        let egui_consumed =
3685            if let (Some(egui_state), Some(window)) = (&mut self.egui_state, &self.window) {
3686                let event_response = egui_state.on_window_event(window, &event);
3687                event_response.consumed
3688            } else {
3689                false
3690            };
3691
3692        // Debug: Log when egui consumes events but we ignore it
3693        if egui_consumed
3694            && !self.settings_ui.visible
3695            && let WindowEvent::KeyboardInput {
3696                event: key_event, ..
3697            } = &event
3698            && let Key::Named(NamedKey::Space) = &key_event.logical_key
3699        {
3700            log::debug!("egui tried to consume Space (UI closed, ignoring)");
3701        }
3702
3703        // Only honor egui's consumption if an egui UI panel is actually visible
3704        // This prevents egui from stealing Tab/Space when UI is closed
3705        let any_ui_visible =
3706            self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
3707        if egui_consumed
3708            && any_ui_visible
3709            && !matches!(
3710                event,
3711                WindowEvent::CloseRequested | WindowEvent::RedrawRequested
3712            )
3713        {
3714            if let WindowEvent::KeyboardInput {
3715                event: key_event, ..
3716            } = &event
3717            {
3718                match &key_event.logical_key {
3719                    Key::Named(NamedKey::Space) => {
3720                        log::debug!("egui consumed Space while UI panel is visible")
3721                    }
3722                    Key::Named(_) => {
3723                        log::debug!("egui consumed named key while UI panel is visible")
3724                    }
3725                    _ => {}
3726                }
3727            }
3728            return;
3729        }
3730
3731        match event {
3732            WindowEvent::CloseRequested => {
3733                log::info!("Close requested, cleaning up and exiting");
3734                // Set shutdown flag to stop redraw loop
3735                self.is_shutting_down = true;
3736                // Abort the refresh task to prevent lockup on shutdown
3737                if let Some(task) = self.refresh_task.take() {
3738                    task.abort();
3739                    log::info!("Refresh task aborted");
3740                }
3741                event_loop.exit();
3742            }
3743
3744            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
3745                if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
3746                    log::info!(
3747                        "Scale factor changed to {} (display change detected)",
3748                        scale_factor
3749                    );
3750
3751                    let size = window.inner_size();
3752                    let (cols, rows) = renderer.handle_scale_factor_change(scale_factor, size);
3753
3754                    // Reconfigure surface after scale factor change
3755                    // This is important when dragging between displays with different DPIs
3756                    renderer.reconfigure_surface();
3757
3758                    // Calculate pixel dimensions
3759                    let cell_width = renderer.cell_width();
3760                    let cell_height = renderer.cell_height();
3761                    let width_px = (cols as f32 * cell_width) as usize;
3762                    let height_px = (rows as f32 * cell_height) as usize;
3763
3764                    // Resize terminal with pixel dimensions for TIOCGWINSZ support
3765                    if let Some(terminal) = &self.terminal
3766                        && let Ok(mut term) = terminal.try_lock()
3767                    {
3768                        let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
3769                    }
3770
3771                    // Reconfigure macOS Metal layer after display change
3772                    #[cfg(target_os = "macos")]
3773                    {
3774                        if let Err(e) =
3775                            crate::macos_metal::configure_metal_layer_for_performance(window)
3776                        {
3777                            log::warn!(
3778                                "Failed to reconfigure Metal layer after display change: {}",
3779                                e
3780                            );
3781                        }
3782                    }
3783
3784                    // Request redraw to apply changes
3785                    window.request_redraw();
3786                }
3787            }
3788
3789            // Handle window moved - surface may become invalid when moving between monitors
3790            WindowEvent::Moved(_) => {
3791                if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
3792                    log::debug!(
3793                        "Window moved - reconfiguring surface for potential display change"
3794                    );
3795
3796                    // Reconfigure surface to handle potential display changes
3797                    // This catches cases where displays have same DPI but different surface properties
3798                    renderer.reconfigure_surface();
3799
3800                    // On macOS, reconfigure the Metal layer for the potentially new display
3801                    #[cfg(target_os = "macos")]
3802                    {
3803                        if let Err(e) =
3804                            crate::macos_metal::configure_metal_layer_for_performance(window)
3805                        {
3806                            log::warn!(
3807                                "Failed to reconfigure Metal layer after window move: {}",
3808                                e
3809                            );
3810                        }
3811                    }
3812
3813                    // Request redraw to ensure proper rendering on new display
3814                    window.request_redraw();
3815                }
3816            }
3817
3818            WindowEvent::Resized(physical_size) => {
3819                if let Some(renderer) = &mut self.renderer {
3820                    let (cols, rows) = renderer.resize(physical_size);
3821
3822                    // Calculate text area pixel dimensions
3823                    let cell_width = renderer.cell_width();
3824                    let cell_height = renderer.cell_height();
3825                    let width_px = (cols as f32 * cell_width) as usize;
3826                    let height_px = (rows as f32 * cell_height) as usize;
3827
3828                    // Resize terminal with pixel dimensions for TIOCGWINSZ support
3829                    // This allows applications like kitty icat to query pixel dimensions
3830                    // Note: The core library (v0.11.0+) implements scrollback reflow when
3831                    // width changes - wrapped lines are unwrapped/re-wrapped as needed.
3832                    if let Some(terminal) = &self.terminal
3833                        && let Ok(mut term) = terminal.try_lock()
3834                    {
3835                        let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
3836                        self.cached_scrollback_len = term.scrollback_len();
3837
3838                        // Update scrollbar internal state
3839                        let total_lines = rows + self.cached_scrollback_len;
3840                        renderer.update_scrollbar(self.scroll_state.offset, rows, total_lines);
3841                    }
3842
3843                    // Invalidate cell cache to force regeneration
3844                    self.cached_cells = None;
3845                }
3846            }
3847
3848            WindowEvent::KeyboardInput { event, .. } => {
3849                self.handle_key_event(event, event_loop);
3850            }
3851
3852            WindowEvent::ModifiersChanged(modifiers) => {
3853                self.input_handler.update_modifiers(modifiers);
3854            }
3855
3856            WindowEvent::MouseWheel { delta, .. } => {
3857                // Skip if egui UI is handling mouse
3858                if !self.is_egui_using_pointer() {
3859                    self.handle_mouse_wheel(delta);
3860                }
3861            }
3862
3863            WindowEvent::MouseInput { button, state, .. } => {
3864                // Skip if egui UI is handling mouse
3865                if !self.is_egui_using_pointer() {
3866                    self.handle_mouse_button(button, state);
3867                }
3868            }
3869
3870            WindowEvent::CursorMoved { position, .. } => {
3871                // Skip if egui UI is handling mouse
3872                if !self.is_egui_using_pointer() {
3873                    self.handle_mouse_move((position.x, position.y));
3874                }
3875            }
3876
3877            WindowEvent::RedrawRequested => {
3878                // Skip rendering if shutting down
3879                if self.is_shutting_down {
3880                    return;
3881                }
3882
3883                // Check if shell has exited and close window if configured
3884                if self.config.exit_on_shell_exit
3885                    && let Some(terminal) = &self.terminal
3886                    && let Ok(term) = terminal.try_lock()
3887                    && !term.is_running()
3888                {
3889                    log::info!("Shell has exited, closing terminal");
3890                    // Set shutdown flag to stop redraw loop
3891                    self.is_shutting_down = true;
3892                    // Abort the refresh task to prevent lockup on shutdown
3893                    if let Some(task) = self.refresh_task.take() {
3894                        task.abort();
3895                        log::info!("Refresh task aborted");
3896                    }
3897                    event_loop.exit();
3898                    return;
3899                }
3900
3901                self.render();
3902            }
3903
3904            _ => {}
3905        }
3906    }
3907
3908    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
3909        // Skip all processing if shutting down
3910        if self.is_shutting_down {
3911            return;
3912        }
3913
3914        // Check for and deliver notifications (OSC 9/777)
3915        self.check_notifications();
3916
3917        // Check for bell events and play audio/visual feedback
3918        self.check_bell();
3919
3920        // Update window title with shell integration info (CWD, exit code)
3921        self.update_window_title_with_shell_integration();
3922
3923        // --- POWER SAVING & SMART REDRAW LOGIC ---
3924        // We use ControlFlow::WaitUntil to sleep until the next expected event.
3925        // This drastically reduces CPU/GPU usage compared to continuous polling (ControlFlow::Poll).
3926        // The loop calculates the earliest time any component needs to update.
3927
3928        let now = std::time::Instant::now();
3929        let mut next_wake = now + std::time::Duration::from_secs(1); // Default sleep for 1s of inactivity
3930
3931        // 1. Cursor Blinking
3932        // Wake up exactly when the cursor needs to toggle visibility or fade.
3933        if self.config.cursor_blink {
3934            if self.cursor_blink_timer.is_none() {
3935                let blink_interval =
3936                    std::time::Duration::from_millis(self.config.cursor_blink_interval);
3937                self.cursor_blink_timer = Some(now + blink_interval);
3938            }
3939
3940            if let Some(next_blink) = self.cursor_blink_timer {
3941                if now >= next_blink {
3942                    // Time to toggle: trigger redraw and schedule next phase
3943                    self.needs_redraw = true;
3944                    let blink_interval =
3945                        std::time::Duration::from_millis(self.config.cursor_blink_interval);
3946                    self.cursor_blink_timer = Some(now + blink_interval);
3947                } else if next_blink < next_wake {
3948                    // Schedule wake-up for the next toggle
3949                    next_wake = next_blink;
3950                }
3951            }
3952        }
3953
3954        // 2. Smooth Scrolling & Animations
3955        // If a scroll interpolation or terminal animation is active, target ~60 FPS (16.6ms).
3956        if self.scroll_state.animation_start.is_some() {
3957            self.needs_redraw = true;
3958            let next_frame = now + std::time::Duration::from_millis(16);
3959            if next_frame < next_wake {
3960                next_wake = next_frame;
3961            }
3962        }
3963
3964        // 3. Visual Bell Feedback
3965        // Maintain high frame rate during the visual flash fade-out.
3966        if self.visual_bell_flash.is_some() {
3967            self.needs_redraw = true;
3968            let next_frame = now + std::time::Duration::from_millis(16);
3969            if next_frame < next_wake {
3970                next_wake = next_frame;
3971            }
3972        }
3973
3974        // 4. Interactive UI Elements
3975        // Ensure high responsiveness during mouse dragging (text selection or scrollbar).
3976        if (self.is_selecting || self.selection.is_some() || self.scroll_state.dragging)
3977            && self.mouse_button_pressed
3978        {
3979            self.needs_redraw = true;
3980        }
3981
3982        // 5. Custom Background Shaders
3983        // If a custom shader is animated, we must render continuously at high FPS.
3984        if let Some(renderer) = &self.renderer
3985            && renderer.needs_continuous_render()
3986        {
3987            self.needs_redraw = true;
3988            let next_frame = now + std::time::Duration::from_millis(16);
3989            if next_frame < next_wake {
3990                next_wake = next_frame;
3991            }
3992        }
3993
3994        // --- TRIGGER REDRAW ---
3995        // Request a redraw if any of the logic above determined an update is due.
3996        if self.needs_redraw
3997            && let Some(window) = &self.window
3998        {
3999            window.request_redraw();
4000            self.needs_redraw = false;
4001        }
4002
4003        // Set the calculated sleep interval
4004        event_loop.set_control_flow(ControlFlow::WaitUntil(next_wake));
4005    }
4006}
4007
4008impl Drop for AppState {
4009    fn drop(&mut self) {
4010        log::info!("Shutting down application");
4011
4012        // Set shutdown flag
4013        self.is_shutting_down = true;
4014
4015        // Abort refresh task first to prevent lock contention
4016        if let Some(handle) = self.refresh_task.take() {
4017            handle.abort();
4018            log::info!("Refresh task aborted");
4019
4020            // Give abort time to take effect and any pending operations to complete
4021            std::thread::sleep(std::time::Duration::from_millis(100));
4022        }
4023
4024        // Kill the PTY process first (doesn't require terminal lock)
4025        if let Some(terminal) = &self.terminal {
4026            // Try to acquire lock briefly to kill the PTY
4027            let killed = if let Ok(mut term) = terminal.try_lock() {
4028                if term.is_running() {
4029                    log::info!("Killing PTY process during shutdown");
4030                    match term.kill() {
4031                        Ok(()) => {
4032                            log::info!("PTY process killed successfully");
4033                            true
4034                        }
4035                        Err(e) => {
4036                            log::warn!("Failed to kill PTY process: {:?}", e);
4037                            false
4038                        }
4039                    }
4040                } else {
4041                    log::info!("PTY process already stopped");
4042                    true
4043                }
4044            } else {
4045                log::warn!("Could not acquire terminal lock to kill PTY during shutdown");
4046                false
4047            };
4048
4049            // Give the PTY time to clean up after kill signal
4050            if killed {
4051                std::thread::sleep(std::time::Duration::from_millis(100));
4052            }
4053        }
4054
4055        // Now drop terminal - should be safe since PTY is killed and refresh task is aborted
4056        if let Some(terminal) = self.terminal.take() {
4057            // Use a shorter timeout since PTY is already killed
4058            let timeout = std::time::Duration::from_millis(500);
4059            let start = std::time::Instant::now();
4060
4061            loop {
4062                if let Ok(_term) = terminal.try_lock() {
4063                    log::info!("Terminal lock acquired for cleanup");
4064                    // Terminal will be dropped when _term goes out of scope
4065                    break;
4066                } else if start.elapsed() >= timeout {
4067                    log::warn!(
4068                        "Could not acquire terminal lock within timeout during shutdown, forcing cleanup"
4069                    );
4070                    // Force drop by breaking - Arc will be dropped anyway
4071                    break;
4072                }
4073                std::thread::sleep(std::time::Duration::from_millis(10));
4074            }
4075            // Arc will be dropped here regardless
4076        }
4077
4078        log::info!("Application shutdown complete");
4079    }
4080}