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