Skip to main content

par_term/app/
handler.rs

1//! Application event handler
2//!
3//! This module implements the winit `ApplicationHandler` trait for `WindowManager`,
4//! routing window events to the appropriate `WindowState` and handling menu events.
5
6use crate::app::window_manager::WindowManager;
7use crate::app::window_state::WindowState;
8use std::sync::Arc;
9use winit::application::ApplicationHandler;
10use winit::event::WindowEvent;
11use winit::event_loop::{ActiveEventLoop, ControlFlow};
12use winit::window::WindowId;
13
14impl WindowState {
15    /// Update window title with shell integration info (cwd and exit code)
16    /// Only updates if not scrolled and not hovering over URL
17    pub(crate) fn update_window_title_with_shell_integration(&self) {
18        // Get active tab state
19        let tab = if let Some(t) = self.tab_manager.active_tab() {
20            t
21        } else {
22            return;
23        };
24
25        // Skip if scrolled (scrollback indicator takes priority)
26        if tab.scroll_state.offset != 0 {
27            return;
28        }
29
30        // Skip if hovering over URL (URL tooltip takes priority)
31        if tab.mouse.hovered_url.is_some() {
32            return;
33        }
34
35        // Skip if window not available
36        let window = if let Some(w) = &self.window {
37            w
38        } else {
39            return;
40        };
41
42        // Try to get shell integration info
43        if let Ok(term) = tab.terminal.try_lock() {
44            let mut title_parts = vec![self.config.window_title.clone()];
45
46            // Add window number if configured
47            if self.config.show_window_number {
48                title_parts.push(format!("[{}]", self.window_index));
49            }
50
51            // Add current working directory if available
52            if let Some(cwd) = term.shell_integration_cwd() {
53                // Abbreviate home directory to ~
54                let abbreviated_cwd = if let Some(home) = dirs::home_dir() {
55                    cwd.replace(&home.to_string_lossy().to_string(), "~")
56                } else {
57                    cwd
58                };
59                title_parts.push(format!("({})", abbreviated_cwd));
60            }
61
62            // Add running command indicator if a command is executing
63            if let Some(cmd_name) = term.get_running_command_name() {
64                title_parts.push(format!("[{}]", cmd_name));
65            }
66
67            // Add exit code indicator if last command failed
68            if let Some(exit_code) = term.shell_integration_exit_code()
69                && exit_code != 0
70            {
71                title_parts.push(format!("[Exit: {}]", exit_code));
72            }
73
74            // Add recording indicator
75            if self.is_recording {
76                title_parts.push("[RECORDING]".to_string());
77            }
78
79            // Build and set title
80            let title = title_parts.join(" ");
81            window.set_title(&title);
82        }
83    }
84
85    /// Sync shell integration data (exit code, command, cwd, hostname, username) to badge variables
86    pub(crate) fn sync_badge_shell_integration(&mut self) {
87        let tab = if let Some(t) = self.tab_manager.active_tab() {
88            t
89        } else {
90            return;
91        };
92
93        if let Ok(term) = tab.terminal.try_lock() {
94            let exit_code = term.shell_integration_exit_code();
95            let current_command = term.get_running_command_name();
96            let cwd = term.shell_integration_cwd();
97            let hostname = term.shell_integration_hostname();
98            let username = term.shell_integration_username();
99
100            let mut vars = self.badge_state.variables_mut();
101            let mut badge_changed = false;
102
103            if vars.exit_code != exit_code {
104                vars.set_exit_code(exit_code);
105                badge_changed = true;
106            }
107            if vars.current_command != current_command {
108                vars.set_current_command(current_command);
109                badge_changed = true;
110            }
111            if let Some(cwd) = cwd
112                && vars.path != cwd
113            {
114                vars.set_path(cwd);
115                badge_changed = true;
116            }
117            if let Some(ref host) = hostname
118                && vars.hostname != *host
119            {
120                vars.hostname = host.clone();
121                badge_changed = true;
122            } else if hostname.is_none() && !vars.hostname.is_empty() {
123                // Returned to localhost — keep the initial hostname from new()
124            }
125            if let Some(ref user) = username
126                && vars.username != *user
127            {
128                vars.username = user.clone();
129                badge_changed = true;
130            }
131            drop(vars);
132            if badge_changed {
133                self.badge_state.mark_dirty();
134            }
135        }
136    }
137
138    /// Handle window events for this window state
139    pub(crate) fn handle_window_event(
140        &mut self,
141        event_loop: &ActiveEventLoop,
142        event: WindowEvent,
143    ) -> bool {
144        use winit::keyboard::{Key, NamedKey};
145
146        // Debug: Log ALL keyboard events at entry to diagnose Space issue
147        if let WindowEvent::KeyboardInput {
148            event: key_event, ..
149        } = &event
150        {
151            match &key_event.logical_key {
152                Key::Character(s) => {
153                    log::trace!(
154                        "window_event: Character '{}', state={:?}",
155                        s,
156                        key_event.state
157                    );
158                }
159                Key::Named(named) => {
160                    log::trace!(
161                        "window_event: Named key {:?}, state={:?}",
162                        named,
163                        key_event.state
164                    );
165                }
166                other => {
167                    log::trace!(
168                        "window_event: Other key {:?}, state={:?}",
169                        other,
170                        key_event.state
171                    );
172                }
173            }
174        }
175
176        // Let egui handle the event (needed for proper rendering state)
177        let (egui_consumed, egui_needs_repaint) =
178            if let (Some(egui_state), Some(window)) = (&mut self.egui_state, &self.window) {
179                let event_response = egui_state.on_window_event(window, &event);
180                // Request redraw if egui needs it (e.g., text input in modals)
181                if event_response.repaint {
182                    window.request_redraw();
183                }
184                (event_response.consumed, event_response.repaint)
185            } else {
186                (false, false)
187            };
188        let _ = egui_needs_repaint; // Used above, silence unused warning
189
190        // Debug: Log when egui consumes events but we ignore it
191        // Note: Settings are handled by standalone SettingsWindow, not embedded UI
192        // Note: Profile drawer does NOT block input - only modal dialogs do
193        let any_ui_visible = self.help_ui.visible
194            || self.clipboard_history_ui.visible
195            || self.command_history_ui.visible
196            || self.shader_install_ui.visible
197            || self.integrations_ui.visible
198            || self.remote_shell_install_ui.is_visible()
199            || self.quit_confirmation_ui.is_visible()
200            || self.ssh_connect_ui.is_visible();
201        if egui_consumed
202            && !any_ui_visible
203            && let WindowEvent::KeyboardInput {
204                event: key_event, ..
205            } = &event
206            && let Key::Named(NamedKey::Space) = &key_event.logical_key
207        {
208            log::debug!("egui tried to consume Space (UI closed, ignoring)");
209        }
210
211        // When shader editor is visible, block keyboard events from terminal
212        // even if egui didn't consume them (egui might not have focus)
213        if any_ui_visible
214            && let WindowEvent::KeyboardInput {
215                event: key_event, ..
216            } = &event
217            // Always block keyboard input when UI is visible (except system keys)
218            && !matches!(
219                key_event.logical_key,
220                Key::Named(NamedKey::F1)
221                    | Key::Named(NamedKey::F2)
222                    | Key::Named(NamedKey::F3)
223                    | Key::Named(NamedKey::F11)
224                    | Key::Named(NamedKey::Escape)
225            )
226        {
227            return false;
228        }
229
230        if egui_consumed
231            && any_ui_visible
232            && !matches!(
233                event,
234                WindowEvent::CloseRequested | WindowEvent::RedrawRequested
235            )
236        {
237            return false; // Event consumed by egui, don't close window
238        }
239
240        match event {
241            WindowEvent::CloseRequested => {
242                log::info!("Close requested for window");
243
244                // Check if prompt_on_quit is enabled and there are active sessions
245                let tab_count = self.tab_manager.tab_count();
246                if self.config.prompt_on_quit
247                    && tab_count > 0
248                    && !self.quit_confirmation_ui.is_visible()
249                {
250                    log::info!(
251                        "Showing quit confirmation dialog ({} active sessions)",
252                        tab_count
253                    );
254                    self.quit_confirmation_ui.show_confirmation(tab_count);
255                    self.needs_redraw = true;
256                    if let Some(window) = &self.window {
257                        window.request_redraw();
258                    }
259                    return false; // Don't close yet - wait for user confirmation
260                }
261
262                self.perform_shutdown();
263                return true; // Signal to close this window
264            }
265
266            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
267                if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
268                    log::info!(
269                        "Scale factor changed to {} (display change detected)",
270                        scale_factor
271                    );
272
273                    let size = window.inner_size();
274                    let (cols, rows) = renderer.handle_scale_factor_change(scale_factor, size);
275
276                    // Reconfigure surface after scale factor change
277                    // This is important when dragging between displays with different DPIs
278                    renderer.reconfigure_surface();
279
280                    // Calculate pixel dimensions
281                    let cell_width = renderer.cell_width();
282                    let cell_height = renderer.cell_height();
283                    let width_px = (cols as f32 * cell_width) as usize;
284                    let height_px = (rows as f32 * cell_height) as usize;
285
286                    // Resize all tabs' terminals with pixel dimensions for TIOCGWINSZ support
287                    for tab in self.tab_manager.tabs_mut() {
288                        if let Ok(mut term) = tab.terminal.try_lock() {
289                            let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
290                        }
291                    }
292
293                    // Reconfigure macOS Metal layer after display change
294                    #[cfg(target_os = "macos")]
295                    {
296                        if let Err(e) =
297                            crate::macos_metal::configure_metal_layer_for_performance(window)
298                        {
299                            log::warn!(
300                                "Failed to reconfigure Metal layer after display change: {}",
301                                e
302                            );
303                        }
304                    }
305
306                    // Request redraw to apply changes
307                    window.request_redraw();
308                }
309            }
310
311            // Handle window moved - surface may become invalid when moving between monitors
312            WindowEvent::Moved(_) => {
313                if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
314                    log::debug!(
315                        "Window moved - reconfiguring surface for potential display change"
316                    );
317
318                    // Reconfigure surface to handle potential display changes
319                    // This catches cases where displays have same DPI but different surface properties
320                    renderer.reconfigure_surface();
321
322                    // On macOS, reconfigure the Metal layer for the potentially new display
323                    #[cfg(target_os = "macos")]
324                    {
325                        if let Err(e) =
326                            crate::macos_metal::configure_metal_layer_for_performance(window)
327                        {
328                            log::warn!(
329                                "Failed to reconfigure Metal layer after window move: {}",
330                                e
331                            );
332                        }
333                    }
334
335                    // Request redraw to ensure proper rendering on new display
336                    window.request_redraw();
337                }
338            }
339
340            WindowEvent::Resized(physical_size) => {
341                if let Some(renderer) = &mut self.renderer {
342                    let (cols, rows) = renderer.resize(physical_size);
343
344                    // Calculate text area pixel dimensions
345                    let cell_width = renderer.cell_width();
346                    let cell_height = renderer.cell_height();
347                    let width_px = (cols as f32 * cell_width) as usize;
348                    let height_px = (rows as f32 * cell_height) as usize;
349
350                    // Resize all tabs' terminals with pixel dimensions for TIOCGWINSZ support
351                    // This allows applications like kitty icat to query pixel dimensions
352                    // Note: The core library (v0.11.0+) implements scrollback reflow when
353                    // width changes - wrapped lines are unwrapped/re-wrapped as needed.
354                    for tab in self.tab_manager.tabs_mut() {
355                        if let Ok(mut term) = tab.terminal.try_lock() {
356                            let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
357                            tab.cache.scrollback_len = term.scrollback_len();
358                        }
359                        // Invalidate cell cache to force regeneration
360                        tab.cache.cells = None;
361                    }
362
363                    // Update scrollbar for active tab
364                    if let Some(tab) = self.tab_manager.active_tab() {
365                        let total_lines = rows + tab.cache.scrollback_len;
366                        let marks = if let Ok(term) = tab.terminal.try_lock() {
367                            term.scrollback_marks()
368                        } else {
369                            Vec::new()
370                        };
371                        renderer.update_scrollbar(
372                            tab.scroll_state.offset,
373                            rows,
374                            total_lines,
375                            &marks,
376                        );
377                    }
378
379                    // Update resize overlay state
380                    self.resize_dimensions =
381                        Some((physical_size.width, physical_size.height, cols, rows));
382                    self.resize_overlay_visible = true;
383                    // Hide overlay 1 second after resize stops
384                    self.resize_overlay_hide_time =
385                        Some(std::time::Instant::now() + std::time::Duration::from_secs(1));
386
387                    // Notify tmux of the new size if gateway mode is active
388                    self.notify_tmux_of_resize();
389                }
390            }
391
392            WindowEvent::KeyboardInput { event, .. } => {
393                self.handle_key_event(event, event_loop);
394            }
395
396            WindowEvent::ModifiersChanged(modifiers) => {
397                self.input_handler.update_modifiers(modifiers);
398            }
399
400            WindowEvent::MouseWheel { delta, .. } => {
401                // Skip terminal handling if egui UI is visible or using the pointer
402                // Note: any_ui_visible check is needed because is_egui_using_pointer()
403                // returns false before egui is initialized (e.g., at startup when
404                // shader_install_ui is shown before first render)
405                if !any_ui_visible && !self.is_egui_using_pointer() {
406                    self.handle_mouse_wheel(delta);
407                }
408            }
409
410            WindowEvent::MouseInput { button, state, .. } => {
411                use winit::event::ElementState;
412
413                // Eat the first mouse press that brings the window into focus.
414                // Without this, the click is forwarded to the PTY where mouse-aware
415                // apps (tmux with `mouse on`) trigger a zero-char selection that
416                // clears the system clipboard — destroying any clipboard image.
417                if self.focus_click_pending && state == ElementState::Pressed {
418                    self.focus_click_pending = false;
419                    self.ui_consumed_mouse_press = true; // Also suppress the release
420                    if let Some(window) = &self.window {
421                        window.request_redraw();
422                    }
423                } else {
424                    // Track UI mouse consumption to prevent release events bleeding through
425                    // when UI closes during a click (e.g., drawer toggle)
426                    let ui_wants_pointer = any_ui_visible || self.is_egui_using_pointer();
427
428                    if state == ElementState::Pressed {
429                        if ui_wants_pointer {
430                            self.ui_consumed_mouse_press = true;
431                            if let Some(window) = &self.window {
432                                window.request_redraw();
433                            }
434                        } else {
435                            self.ui_consumed_mouse_press = false;
436                            self.handle_mouse_button(button, state);
437                        }
438                    } else {
439                        // Release: block if we consumed the press OR if UI wants pointer
440                        if self.ui_consumed_mouse_press || ui_wants_pointer {
441                            self.ui_consumed_mouse_press = false;
442                            if let Some(window) = &self.window {
443                                window.request_redraw();
444                            }
445                        } else {
446                            self.handle_mouse_button(button, state);
447                        }
448                    }
449                }
450            }
451
452            WindowEvent::CursorMoved { position, .. } => {
453                // Skip terminal handling if egui UI is visible or using the pointer
454                if any_ui_visible || self.is_egui_using_pointer() {
455                    // Request redraw so egui can update hover states
456                    if let Some(window) = &self.window {
457                        window.request_redraw();
458                    }
459                } else {
460                    self.handle_mouse_move((position.x, position.y));
461                }
462            }
463
464            WindowEvent::Focused(focused) => {
465                self.handle_focus_change(focused);
466            }
467
468            WindowEvent::RedrawRequested => {
469                // Skip rendering if shutting down
470                if self.is_shutting_down {
471                    return false;
472                }
473
474                // Handle shell exit based on configured action
475                use crate::config::ShellExitAction;
476                use crate::pane::RestartState;
477
478                match self.config.shell_exit_action {
479                    ShellExitAction::Keep => {
480                        // Do nothing - keep dead shells showing
481                    }
482
483                    ShellExitAction::Close => {
484                        // Original behavior: close exited panes and their tabs
485                        let mut tabs_needing_resize: Vec<crate::tab::TabId> = Vec::new();
486
487                        let tabs_to_close: Vec<crate::tab::TabId> = self
488                            .tab_manager
489                            .tabs_mut()
490                            .iter_mut()
491                            .filter_map(|tab| {
492                                if tab.tmux_gateway_active || tab.tmux_pane_id.is_some() {
493                                    return None;
494                                }
495                                if tab.pane_manager.is_some() {
496                                    let (closed_panes, tab_should_close) = tab.close_exited_panes();
497                                    if !closed_panes.is_empty() {
498                                        log::info!(
499                                            "Tab {}: closed {} exited pane(s)",
500                                            tab.id,
501                                            closed_panes.len()
502                                        );
503                                        if !tab_should_close {
504                                            tabs_needing_resize.push(tab.id);
505                                        }
506                                    }
507                                    if tab_should_close {
508                                        return Some(tab.id);
509                                    }
510                                }
511                                None
512                            })
513                            .collect();
514
515                        if !tabs_needing_resize.is_empty()
516                            && let Some(renderer) = &self.renderer
517                        {
518                            let cell_width = renderer.cell_width();
519                            let cell_height = renderer.cell_height();
520                            let padding = self.config.pane_padding;
521                            let title_offset = if self.config.show_pane_titles {
522                                self.config.pane_title_height
523                            } else {
524                                0.0
525                            };
526                            for tab_id in tabs_needing_resize {
527                                if let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
528                                    && let Some(pm) = tab.pane_manager_mut()
529                                {
530                                    pm.resize_all_terminals_with_padding(
531                                        cell_width,
532                                        cell_height,
533                                        padding,
534                                        title_offset,
535                                    );
536                                }
537                            }
538                        }
539
540                        for tab_id in &tabs_to_close {
541                            log::info!("Closing tab {} - all panes exited", tab_id);
542                            if self.tab_manager.tab_count() <= 1 {
543                                log::info!("Last tab, closing window");
544                                self.is_shutting_down = true;
545                                for tab in self.tab_manager.tabs_mut() {
546                                    tab.stop_refresh_task();
547                                }
548                                return true;
549                            } else {
550                                let _ = self.tab_manager.close_tab(*tab_id);
551                            }
552                        }
553
554                        // Also check legacy single-pane tabs
555                        let (shell_exited, active_tab_id, tab_count, tab_title, exit_notified) = {
556                            if let Some(tab) = self.tab_manager.active_tab() {
557                                let exited = tab.pane_manager.is_none()
558                                    && tab
559                                        .terminal
560                                        .try_lock()
561                                        .ok()
562                                        .is_some_and(|term| !term.is_running());
563                                (
564                                    exited,
565                                    Some(tab.id),
566                                    self.tab_manager.tab_count(),
567                                    tab.title.clone(),
568                                    tab.exit_notified,
569                                )
570                            } else {
571                                (false, None, 0, String::new(), false)
572                            }
573                        };
574
575                        if shell_exited {
576                            log::info!("Shell in active tab has exited");
577                            if self.config.notification_session_ended && !exit_notified {
578                                if let Some(tab) = self.tab_manager.active_tab_mut() {
579                                    tab.exit_notified = true;
580                                }
581                                let title = format!("Session Ended: {}", tab_title);
582                                let message = "The shell process has exited".to_string();
583                                self.deliver_notification(&title, &message);
584                            }
585
586                            if tab_count <= 1 {
587                                log::info!("Last tab, closing window");
588                                self.is_shutting_down = true;
589                                for tab in self.tab_manager.tabs_mut() {
590                                    tab.stop_refresh_task();
591                                }
592                                return true;
593                            } else if let Some(tab_id) = active_tab_id {
594                                let _ = self.tab_manager.close_tab(tab_id);
595                            }
596                        }
597                    }
598
599                    ShellExitAction::RestartImmediately
600                    | ShellExitAction::RestartWithPrompt
601                    | ShellExitAction::RestartAfterDelay => {
602                        // Handle restart variants
603                        let config_clone = self.config.clone();
604
605                        for tab in self.tab_manager.tabs_mut() {
606                            if tab.tmux_gateway_active || tab.tmux_pane_id.is_some() {
607                                continue;
608                            }
609
610                            if let Some(pm) = tab.pane_manager_mut() {
611                                for pane in pm.all_panes_mut() {
612                                    let is_running = pane.is_running();
613
614                                    // Check if pane needs restart action
615                                    if !is_running && pane.restart_state.is_none() {
616                                        // Shell just exited, handle based on action
617                                        match self.config.shell_exit_action {
618                                            ShellExitAction::RestartImmediately => {
619                                                log::info!(
620                                                    "Pane {} shell exited, restarting immediately",
621                                                    pane.id
622                                                );
623                                                if let Err(e) = pane.respawn_shell(&config_clone) {
624                                                    log::error!(
625                                                        "Failed to respawn shell in pane {}: {}",
626                                                        pane.id,
627                                                        e
628                                                    );
629                                                }
630                                            }
631                                            ShellExitAction::RestartWithPrompt => {
632                                                log::info!(
633                                                    "Pane {} shell exited, showing restart prompt",
634                                                    pane.id
635                                                );
636                                                pane.write_restart_prompt();
637                                                pane.restart_state =
638                                                    Some(RestartState::AwaitingInput);
639                                            }
640                                            ShellExitAction::RestartAfterDelay => {
641                                                log::info!(
642                                                    "Pane {} shell exited, will restart after 1s",
643                                                    pane.id
644                                                );
645                                                pane.restart_state =
646                                                    Some(RestartState::AwaitingDelay(
647                                                        std::time::Instant::now(),
648                                                    ));
649                                            }
650                                            _ => {}
651                                        }
652                                    }
653
654                                    // Check if waiting for delay and time has elapsed
655                                    if let Some(RestartState::AwaitingDelay(exit_time)) =
656                                        &pane.restart_state
657                                        && exit_time.elapsed() >= std::time::Duration::from_secs(1)
658                                    {
659                                        log::info!(
660                                            "Pane {} delay elapsed, restarting shell",
661                                            pane.id
662                                        );
663                                        if let Err(e) = pane.respawn_shell(&config_clone) {
664                                            log::error!(
665                                                "Failed to respawn shell in pane {}: {}",
666                                                pane.id,
667                                                e
668                                            );
669                                        }
670                                    }
671                                }
672                            }
673                        }
674                    }
675                }
676
677                self.render();
678            }
679
680            WindowEvent::DroppedFile(path) => {
681                self.handle_dropped_file(path);
682            }
683
684            WindowEvent::CursorEntered { .. } => {
685                // Focus follows mouse: auto-focus window when cursor enters
686                if self.config.focus_follows_mouse
687                    && let Some(window) = &self.window
688                {
689                    window.focus_window();
690                }
691            }
692
693            WindowEvent::ThemeChanged(system_theme) => {
694                let is_dark = system_theme == winit::window::Theme::Dark;
695                let theme_changed = self.config.apply_system_theme(is_dark);
696                let tab_style_changed = self.config.apply_system_tab_style(is_dark);
697
698                if theme_changed {
699                    log::info!(
700                        "System theme changed to {}, switching to theme: {}",
701                        if is_dark { "dark" } else { "light" },
702                        self.config.theme
703                    );
704                    let theme = self.config.load_theme();
705                    for tab in self.tab_manager.tabs_mut() {
706                        if let Ok(mut term) = tab.terminal.try_lock() {
707                            term.set_theme(theme.clone());
708                        }
709                        tab.cache.cells = None;
710                    }
711                }
712
713                if tab_style_changed {
714                    log::info!(
715                        "Auto tab style: switching to {} tab style",
716                        if is_dark {
717                            self.config.dark_tab_style.display_name()
718                        } else {
719                            self.config.light_tab_style.display_name()
720                        }
721                    );
722                }
723
724                if theme_changed || tab_style_changed {
725                    if let Err(e) = self.config.save() {
726                        log::error!("Failed to save config after theme change: {}", e);
727                    }
728                    self.needs_redraw = true;
729                    self.request_redraw();
730                }
731            }
732
733            _ => {}
734        }
735
736        false // Don't close window
737    }
738
739    /// Handle window focus change for power saving
740    pub(crate) fn handle_focus_change(&mut self, focused: bool) {
741        if self.is_focused == focused {
742            return; // No change
743        }
744
745        self.is_focused = focused;
746
747        log::info!(
748            "Window focus changed: {}",
749            if focused { "focused" } else { "blurred" }
750        );
751
752        // Suppress the first mouse click after gaining focus to prevent it from
753        // being forwarded to the PTY. Without this, clicking to focus sends a
754        // mouse event to tmux (or other mouse-aware apps), which can trigger a
755        // zero-char selection that clears the system clipboard.
756        if focused {
757            self.focus_click_pending = true;
758        }
759
760        // Update renderer focus state for unfocused cursor styling
761        if let Some(renderer) = &mut self.renderer {
762            renderer.set_focused(focused);
763        }
764
765        // Handle shader animation pause/resume
766        if self.config.pause_shaders_on_blur
767            && let Some(renderer) = &mut self.renderer
768        {
769            if focused {
770                // Only resume if user has animation enabled in config
771                renderer.resume_shader_animations(
772                    self.config.custom_shader_animation,
773                    self.config.cursor_shader_animation,
774                );
775            } else {
776                renderer.pause_shader_animations();
777            }
778        }
779
780        // Re-assert tmux client size when window gains focus
781        // This ensures par-term's size is respected even after other clients resize tmux
782        if focused {
783            self.notify_tmux_of_resize();
784        }
785
786        // Forward focus events to all PTYs that have focus tracking enabled (DECSET 1004)
787        // This is needed for applications like tmux that rely on focus events
788        for tab in self.tab_manager.tabs_mut() {
789            if let Ok(term) = tab.terminal.try_lock() {
790                term.report_focus_change(focused);
791            }
792            // Also forward to all panes if split panes are active
793            if let Some(pm) = &tab.pane_manager {
794                for pane in pm.all_panes() {
795                    if let Ok(term) = pane.terminal.try_lock() {
796                        term.report_focus_change(focused);
797                    }
798                }
799            }
800        }
801
802        // Handle refresh rate adjustment for all tabs
803        if self.config.pause_refresh_on_blur
804            && let Some(window) = &self.window
805        {
806            let fps = if focused {
807                self.config.max_fps
808            } else {
809                self.config.unfocused_fps
810            };
811            for tab in self.tab_manager.tabs_mut() {
812                tab.stop_refresh_task();
813                tab.start_refresh_task(Arc::clone(&self.runtime), Arc::clone(window), fps);
814            }
815            log::info!(
816                "Adjusted refresh rate to {} FPS ({})",
817                fps,
818                if focused { "focused" } else { "unfocused" }
819            );
820        }
821
822        // Request a redraw when focus changes
823        self.needs_redraw = true;
824        self.request_redraw();
825    }
826
827    /// Process per-window updates in about_to_wait
828    pub(crate) fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
829        // Skip all processing if shutting down
830        if self.is_shutting_down {
831            return;
832        }
833
834        // Check for and deliver notifications (OSC 9/777)
835        self.check_notifications();
836
837        // Check for file transfer events (downloads, uploads, progress)
838        self.check_file_transfers();
839
840        // Check for bell events and play audio/visual feedback
841        self.check_bell();
842
843        // Check for trigger action results and dispatch them
844        self.check_trigger_actions();
845
846        // Check for activity/idle notifications
847        self.check_activity_idle_notifications();
848
849        // Check for session exit notifications
850        self.check_session_exit_notifications();
851
852        // Check for shader hot reload events
853        if self.check_shader_reload() {
854            log::debug!("Shader hot reload triggered redraw");
855        }
856
857        // Check for config file changes (e.g., from ACP agent)
858        self.check_config_reload();
859
860        // Check for MCP server config updates (.config-update.json)
861        self.check_config_update_file();
862
863        // Check for tmux control mode notifications
864        if self.check_tmux_notifications() {
865            self.needs_redraw = true;
866        }
867
868        // Update window title with shell integration info (CWD, exit code)
869        self.update_window_title_with_shell_integration();
870
871        // Sync shell integration data to badge variables
872        self.sync_badge_shell_integration();
873
874        // Check for automatic profile switching based on hostname detection (OSC 7)
875        if self.check_auto_profile_switch() {
876            self.needs_redraw = true;
877        }
878
879        // --- POWER SAVING & SMART REDRAW LOGIC ---
880        // We use ControlFlow::WaitUntil to sleep until the next expected event.
881        // This drastically reduces CPU/GPU usage compared to continuous polling (ControlFlow::Poll).
882        // The loop calculates the earliest time any component needs to update.
883
884        let now = std::time::Instant::now();
885        let mut next_wake = now + std::time::Duration::from_secs(1); // Default sleep for 1s of inactivity
886
887        // Calculate frame interval based on focus state for power saving
888        // When pause_refresh_on_blur is enabled and window is unfocused, use slower refresh rate
889        let frame_interval_ms = if self.config.pause_refresh_on_blur && !self.is_focused {
890            // Use unfocused FPS (e.g., 10 FPS = 100ms interval)
891            1000 / self.config.unfocused_fps.max(1)
892        } else {
893            // Use normal animation rate based on max_fps
894            1000 / self.config.max_fps.max(1)
895        };
896        let frame_interval = std::time::Duration::from_millis(frame_interval_ms as u64);
897
898        // Check if enough time has passed since last render for FPS throttling
899        let time_since_last_render = self
900            .last_render_time
901            .map(|t| now.duration_since(t))
902            .unwrap_or(frame_interval); // If no last render, allow immediate render
903        let can_render = time_since_last_render >= frame_interval;
904
905        // --- FLICKER REDUCTION LOGIC ---
906        // When reduce_flicker is enabled and cursor is hidden, delay rendering
907        // to batch updates and reduce visual flicker during bulk terminal operations.
908        let should_delay_for_flicker = if self.config.reduce_flicker {
909            let cursor_hidden = if let Some(tab) = self.tab_manager.active_tab() {
910                if let Ok(term) = tab.terminal.try_lock() {
911                    !term.is_cursor_visible() && !self.config.lock_cursor_visibility
912                } else {
913                    false
914                }
915            } else {
916                false
917            };
918
919            if cursor_hidden {
920                // Track when cursor was first hidden
921                if self.cursor_hidden_since.is_none() {
922                    self.cursor_hidden_since = Some(now);
923                }
924
925                // Check bypass conditions
926                let delay_expired = self
927                    .cursor_hidden_since
928                    .map(|t| {
929                        now.duration_since(t)
930                            >= std::time::Duration::from_millis(
931                                self.config.reduce_flicker_delay_ms as u64,
932                            )
933                    })
934                    .unwrap_or(false);
935
936                // Bypass for UI interactions
937                let any_ui_visible = self.help_ui.visible
938                    || self.clipboard_history_ui.visible
939                    || self.command_history_ui.visible
940                    || self.search_ui.visible
941                    || self.shader_install_ui.visible
942                    || self.integrations_ui.visible
943                    || self.remote_shell_install_ui.is_visible()
944                    || self.quit_confirmation_ui.is_visible()
945                    || self.ssh_connect_ui.is_visible()
946                    || self.resize_overlay_visible;
947
948                // Delay unless bypass conditions met
949                !delay_expired && !any_ui_visible
950            } else {
951                // Cursor visible - clear tracking and allow render
952                if self.cursor_hidden_since.is_some() {
953                    self.cursor_hidden_since = None;
954                    self.flicker_pending_render = false;
955                    self.needs_redraw = true; // Render accumulated updates
956                }
957                false
958            }
959        } else {
960            false
961        };
962
963        // Schedule wake at delay expiry if delaying
964        if should_delay_for_flicker {
965            self.flicker_pending_render = true;
966            if let Some(hidden_since) = self.cursor_hidden_since {
967                let delay =
968                    std::time::Duration::from_millis(self.config.reduce_flicker_delay_ms as u64);
969                let render_time = hidden_since + delay;
970                if render_time < next_wake {
971                    next_wake = render_time;
972                }
973            }
974        } else if self.flicker_pending_render {
975            // Delay ended - trigger accumulated render
976            self.flicker_pending_render = false;
977            if can_render {
978                self.needs_redraw = true;
979            }
980        }
981
982        // --- THROUGHPUT MODE LOGIC ---
983        // When maximize_throughput is enabled, always batch renders regardless of cursor state.
984        // Uses a longer interval than flicker reduction for better throughput during bulk output.
985        let should_delay_for_throughput = if self.config.maximize_throughput {
986            // Initialize batch start time if not set
987            if self.throughput_batch_start.is_none() {
988                self.throughput_batch_start = Some(now);
989            }
990
991            let interval =
992                std::time::Duration::from_millis(self.config.throughput_render_interval_ms as u64);
993            let batch_start = self.throughput_batch_start.unwrap();
994
995            // Check if interval has elapsed
996            if now.duration_since(batch_start) >= interval {
997                self.throughput_batch_start = Some(now); // Reset for next batch
998                false // Allow render
999            } else {
1000                true // Delay render
1001            }
1002        } else {
1003            // Clear tracking when disabled
1004            if self.throughput_batch_start.is_some() {
1005                self.throughput_batch_start = None;
1006            }
1007            false
1008        };
1009
1010        // Schedule wake for throughput mode
1011        if should_delay_for_throughput && let Some(batch_start) = self.throughput_batch_start {
1012            let interval =
1013                std::time::Duration::from_millis(self.config.throughput_render_interval_ms as u64);
1014            let render_time = batch_start + interval;
1015            if render_time < next_wake {
1016                next_wake = render_time;
1017            }
1018        }
1019
1020        // Combine delays: throughput mode OR flicker delay
1021        let should_delay_render = should_delay_for_throughput || should_delay_for_flicker;
1022
1023        // 1. Cursor Blinking
1024        // Wake up exactly when the cursor needs to toggle visibility or fade.
1025        // Skip cursor blinking when unfocused with pause_refresh_on_blur to save power.
1026        if self.config.cursor_blink && (self.is_focused || !self.config.pause_refresh_on_blur) {
1027            if self.cursor_blink_timer.is_none() {
1028                let blink_interval =
1029                    std::time::Duration::from_millis(self.config.cursor_blink_interval);
1030                self.cursor_blink_timer = Some(now + blink_interval);
1031            }
1032
1033            if let Some(next_blink) = self.cursor_blink_timer {
1034                if now >= next_blink {
1035                    // Time to toggle: trigger redraw (if throttle allows) and schedule next phase
1036                    if can_render {
1037                        self.needs_redraw = true;
1038                    }
1039                    let blink_interval =
1040                        std::time::Duration::from_millis(self.config.cursor_blink_interval);
1041                    self.cursor_blink_timer = Some(now + blink_interval);
1042                } else if next_blink < next_wake {
1043                    // Schedule wake-up for the next toggle
1044                    next_wake = next_blink;
1045                }
1046            }
1047        }
1048
1049        // 2. Smooth Scrolling & Animations
1050        // If a scroll interpolation or terminal animation is active, use calculated frame interval.
1051        if let Some(tab) = self.tab_manager.active_tab() {
1052            if tab.scroll_state.animation_start.is_some() {
1053                if can_render {
1054                    self.needs_redraw = true;
1055                }
1056                let next_frame = now + frame_interval;
1057                if next_frame < next_wake {
1058                    next_wake = next_frame;
1059                }
1060            }
1061
1062            // 3. Visual Bell Feedback
1063            // Maintain frame rate during the visual flash fade-out.
1064            if tab.bell.visual_flash.is_some() {
1065                if can_render {
1066                    self.needs_redraw = true;
1067                }
1068                let next_frame = now + frame_interval;
1069                if next_frame < next_wake {
1070                    next_wake = next_frame;
1071                }
1072            }
1073
1074            // 4. Interactive UI Elements
1075            // Ensure high responsiveness during mouse dragging (text selection or scrollbar).
1076            // Always allow these for responsiveness, even if throttled.
1077            if (tab.mouse.is_selecting
1078                || tab.mouse.selection.is_some()
1079                || tab.scroll_state.dragging)
1080                && tab.mouse.button_pressed
1081            {
1082                self.needs_redraw = true;
1083            }
1084        }
1085
1086        // 5. Resize Overlay
1087        // Check if the resize overlay should be hidden (timer expired).
1088        if self.resize_overlay_visible
1089            && let Some(hide_time) = self.resize_overlay_hide_time
1090        {
1091            if now >= hide_time {
1092                // Hide the overlay
1093                self.resize_overlay_visible = false;
1094                self.resize_overlay_hide_time = None;
1095                self.needs_redraw = true;
1096            } else {
1097                // Overlay still visible - request redraw and schedule wake
1098                if can_render {
1099                    self.needs_redraw = true;
1100                }
1101                if hide_time < next_wake {
1102                    next_wake = hide_time;
1103                }
1104            }
1105        }
1106
1107        // 5b. Toast Notification
1108        // Check if the toast notification should be hidden (timer expired).
1109        if self.toast_message.is_some()
1110            && let Some(hide_time) = self.toast_hide_time
1111        {
1112            if now >= hide_time {
1113                // Hide the toast
1114                self.toast_message = None;
1115                self.toast_hide_time = None;
1116                self.needs_redraw = true;
1117            } else {
1118                // Toast still visible - request redraw and schedule wake
1119                if can_render {
1120                    self.needs_redraw = true;
1121                }
1122                if hide_time < next_wake {
1123                    next_wake = hide_time;
1124                }
1125            }
1126        }
1127
1128        // 5c. Pane Identification Overlay
1129        // Check if the pane index overlay should be hidden (timer expired).
1130        if let Some(hide_time) = self.pane_identify_hide_time {
1131            if now >= hide_time {
1132                self.pane_identify_hide_time = None;
1133                self.needs_redraw = true;
1134            } else {
1135                if can_render {
1136                    self.needs_redraw = true;
1137                }
1138                if hide_time < next_wake {
1139                    next_wake = hide_time;
1140                }
1141            }
1142        }
1143
1144        // 5b. Session undo expiry: prune closed tab metadata that has timed out
1145        if !self.closed_tabs.is_empty() && self.config.session_undo_timeout_secs > 0 {
1146            let timeout =
1147                std::time::Duration::from_secs(self.config.session_undo_timeout_secs as u64);
1148            self.closed_tabs
1149                .retain(|info| now.duration_since(info.closed_at) < timeout);
1150        }
1151
1152        // 6. Custom Background Shaders
1153        // If a custom shader is animated, render at the calculated frame interval.
1154        // When unfocused with pause_refresh_on_blur, this uses the slower unfocused_fps rate.
1155        if let Some(renderer) = &self.renderer
1156            && renderer.needs_continuous_render()
1157        {
1158            if can_render {
1159                self.needs_redraw = true;
1160            }
1161            // Schedule next frame at the appropriate interval
1162            let next_frame = self
1163                .last_render_time
1164                .map(|t| t + frame_interval)
1165                .unwrap_or(now);
1166            // Ensure we don't schedule in the past
1167            let next_frame = if next_frame <= now {
1168                now + frame_interval
1169            } else {
1170                next_frame
1171            };
1172            if next_frame < next_wake {
1173                next_wake = next_frame;
1174            }
1175        }
1176
1177        // 7. Shader Install Dialog
1178        // Force continuous redraws when shader install dialog is visible (for spinner animation)
1179        // and when installation is in progress (to check for completion)
1180        if self.shader_install_ui.visible {
1181            self.needs_redraw = true;
1182            // Schedule frequent redraws for smooth spinner animation
1183            let next_frame = now + std::time::Duration::from_millis(16); // ~60fps
1184            if next_frame < next_wake {
1185                next_wake = next_frame;
1186            }
1187        }
1188
1189        // 8. File Transfer Progress
1190        // Ensure rendering during active file transfers so the progress overlay
1191        // updates. Uses 1-second interval since progress doesn't need smooth animation.
1192        // Bypasses render delays (flicker/throughput) for responsive UI feedback.
1193        let has_active_file_transfers = !self.file_transfer_state.active_uploads.is_empty()
1194            || !self.file_transfer_state.recent_transfers.is_empty();
1195        if has_active_file_transfers {
1196            self.needs_redraw = true;
1197            // Schedule 1 FPS rendering for progress bar updates
1198            let next_frame = now + std::time::Duration::from_secs(1);
1199            if next_frame < next_wake {
1200                next_wake = next_frame;
1201            }
1202        }
1203
1204        // 9. Anti-idle Keep-alive
1205        // Periodically send keep-alive codes to prevent SSH/connection timeouts.
1206        if let Some(next_anti_idle) = self.handle_anti_idle(now)
1207            && next_anti_idle < next_wake
1208        {
1209            next_wake = next_anti_idle;
1210        }
1211
1212        // --- TRIGGER REDRAW ---
1213        // Request a redraw if any of the logic above determined an update is due.
1214        // Respect combined delay (throughput mode OR flicker reduction),
1215        // but bypass delays for active file transfers that need UI feedback.
1216        if self.needs_redraw
1217            && (!should_delay_render || has_active_file_transfers)
1218            && let Some(window) = &self.window
1219        {
1220            window.request_redraw();
1221            self.needs_redraw = false;
1222        }
1223
1224        // Set the calculated sleep interval.
1225        // Use Poll mode during active file transfers — WaitUntil prevents
1226        // RedrawRequested events from being delivered on macOS when PTY data
1227        // events keep the event loop busy.
1228        if has_active_file_transfers {
1229            event_loop.set_control_flow(ControlFlow::Poll);
1230        } else {
1231            event_loop.set_control_flow(ControlFlow::WaitUntil(next_wake));
1232        }
1233    }
1234}
1235
1236impl ApplicationHandler for WindowManager {
1237    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1238        // Create the first window on app resume (or if all windows were closed on some platforms)
1239        if self.windows.is_empty() {
1240            if !self.auto_restore_done {
1241                self.auto_restore_done = true;
1242
1243                // Session restore takes precedence when enabled
1244                if self.config.restore_session && self.restore_session(event_loop) {
1245                    return;
1246                }
1247
1248                // Try auto-restore arrangement if configured
1249                if let Some(ref name) = self.config.auto_restore_arrangement.clone()
1250                    && !name.is_empty()
1251                    && self.arrangement_manager.find_by_name(name).is_some()
1252                {
1253                    log::info!("Auto-restoring arrangement: {}", name);
1254                    self.restore_arrangement_by_name(name, event_loop);
1255                    return;
1256                }
1257            }
1258            self.create_window(event_loop);
1259        }
1260    }
1261
1262    fn window_event(
1263        &mut self,
1264        event_loop: &ActiveEventLoop,
1265        window_id: WindowId,
1266        event: WindowEvent,
1267    ) {
1268        // Check if this event is for the settings window
1269        if self.is_settings_window(window_id) {
1270            if let Some(action) = self.handle_settings_window_event(event) {
1271                use crate::settings_window::SettingsWindowAction;
1272                match action {
1273                    SettingsWindowAction::Close => {
1274                        // Already handled in handle_settings_window_event
1275                    }
1276                    SettingsWindowAction::ApplyConfig(config) => {
1277                        // Apply live config changes to all terminal windows
1278                        log::info!("SETTINGS: ApplyConfig shader={:?}", config.custom_shader);
1279                        self.apply_config_to_windows(&config);
1280                    }
1281                    SettingsWindowAction::SaveConfig(config) => {
1282                        // Save config to disk and apply to all windows
1283                        if let Err(e) = config.save() {
1284                            log::error!("Failed to save config: {}", e);
1285                        } else {
1286                            log::info!("Configuration saved successfully");
1287                        }
1288                        self.apply_config_to_windows(&config);
1289                        // Update settings window with saved config
1290                        if let Some(settings_window) = &mut self.settings_window {
1291                            settings_window.update_config(config);
1292                        }
1293                    }
1294                    SettingsWindowAction::ApplyShader(shader_result) => {
1295                        let _ = self.apply_shader_from_editor(&shader_result.source);
1296                    }
1297                    SettingsWindowAction::ApplyCursorShader(cursor_shader_result) => {
1298                        let _ = self.apply_cursor_shader_from_editor(&cursor_shader_result.source);
1299                    }
1300                    SettingsWindowAction::TestNotification => {
1301                        // Send a test notification to verify permissions
1302                        self.send_test_notification();
1303                    }
1304                    SettingsWindowAction::SaveProfiles(profiles) => {
1305                        // Apply saved profiles to all terminal windows
1306                        for window_state in self.windows.values_mut() {
1307                            window_state.apply_profile_changes(profiles.clone());
1308                        }
1309                        // Update the profiles menu
1310                        if let Some(menu) = &mut self.menu {
1311                            let profile_refs: Vec<&crate::profile::Profile> =
1312                                profiles.iter().collect();
1313                            menu.update_profiles(&profile_refs);
1314                        }
1315                    }
1316                    SettingsWindowAction::OpenProfile(id) => {
1317                        // Open profile in the focused terminal window
1318                        if let Some(window_id) = self.get_focused_window_id()
1319                            && let Some(window_state) = self.windows.get_mut(&window_id)
1320                        {
1321                            window_state.open_profile(id);
1322                        }
1323                    }
1324                    SettingsWindowAction::StartCoprocess(index) => {
1325                        log::debug!("Handler: received StartCoprocess({})", index);
1326                        self.start_coprocess(index);
1327                    }
1328                    SettingsWindowAction::StopCoprocess(index) => {
1329                        log::debug!("Handler: received StopCoprocess({})", index);
1330                        self.stop_coprocess(index);
1331                    }
1332                    SettingsWindowAction::StartScript(index) => {
1333                        crate::debug_info!("SCRIPT", "Handler: received StartScript({})", index);
1334                        self.start_script(index);
1335                    }
1336                    SettingsWindowAction::StopScript(index) => {
1337                        log::debug!("Handler: received StopScript({})", index);
1338                        self.stop_script(index);
1339                    }
1340                    SettingsWindowAction::OpenLogFile => {
1341                        let log_path = crate::debug::log_path();
1342                        log::info!("Opening log file: {}", log_path.display());
1343                        if let Err(e) = open::that(&log_path) {
1344                            log::error!("Failed to open log file: {}", e);
1345                        }
1346                    }
1347                    SettingsWindowAction::SaveArrangement(name) => {
1348                        self.save_arrangement(name, event_loop);
1349                    }
1350                    SettingsWindowAction::RestoreArrangement(id) => {
1351                        self.restore_arrangement(id, event_loop);
1352                    }
1353                    SettingsWindowAction::DeleteArrangement(id) => {
1354                        self.delete_arrangement(id);
1355                    }
1356                    SettingsWindowAction::RenameArrangement(id, new_name) => {
1357                        // Special sentinel values for reorder operations
1358                        if new_name == "__move_up__" {
1359                            self.move_arrangement_up(id);
1360                        } else if new_name == "__move_down__" {
1361                            self.move_arrangement_down(id);
1362                        } else {
1363                            self.rename_arrangement(id, new_name);
1364                        }
1365                    }
1366                    SettingsWindowAction::ForceUpdateCheck => {
1367                        self.force_update_check_for_settings();
1368                    }
1369                    SettingsWindowAction::InstallUpdate(_version) => {
1370                        // The update is handled asynchronously inside SettingsUI.
1371                        // The InstallUpdate action is emitted for logging purposes.
1372                        log::info!("Self-update initiated from settings UI");
1373                    }
1374                    SettingsWindowAction::IdentifyPanes => {
1375                        // Flash pane index overlays on all terminal windows
1376                        for window_state in self.windows.values_mut() {
1377                            window_state.show_pane_indices(std::time::Duration::from_secs(3));
1378                        }
1379                    }
1380                    SettingsWindowAction::InstallShellIntegration => {
1381                        match crate::shell_integration_installer::install(None) {
1382                            Ok(result) => {
1383                                log::info!(
1384                                    "Shell integration installed for {:?} at {:?}",
1385                                    result.shell,
1386                                    result.script_path
1387                                );
1388                                if let Some(sw) = &mut self.settings_window {
1389                                    sw.request_redraw();
1390                                }
1391                            }
1392                            Err(e) => {
1393                                log::error!("Failed to install shell integration: {}", e);
1394                            }
1395                        }
1396                    }
1397                    SettingsWindowAction::UninstallShellIntegration => {
1398                        match crate::shell_integration_installer::uninstall() {
1399                            Ok(_) => {
1400                                log::info!("Shell integration uninstalled");
1401                                if let Some(sw) = &mut self.settings_window {
1402                                    sw.request_redraw();
1403                                }
1404                            }
1405                            Err(e) => {
1406                                log::error!("Failed to uninstall shell integration: {}", e);
1407                            }
1408                        }
1409                    }
1410                    SettingsWindowAction::None => {}
1411                }
1412            }
1413            return;
1414        }
1415
1416        // Check if this is a resize event (before the event is consumed)
1417        let is_resize = matches!(event, WindowEvent::Resized(_));
1418
1419        // Route event to the appropriate terminal window
1420        let (should_close, shader_states, grid_size) =
1421            if let Some(window_state) = self.windows.get_mut(&window_id) {
1422                let close = window_state.handle_window_event(event_loop, event);
1423                // Capture shader states to sync to settings window
1424                let states = (
1425                    window_state.config.custom_shader_enabled,
1426                    window_state.config.cursor_shader_enabled,
1427                );
1428                // Capture grid size if this was a resize
1429                let size = if is_resize {
1430                    window_state.renderer.as_ref().map(|r| r.grid_size())
1431                } else {
1432                    None
1433                };
1434                (close, Some(states), size)
1435            } else {
1436                (false, None, None)
1437            };
1438
1439        // Sync shader states to settings window to prevent it from overwriting keybinding toggles
1440        if let (Some(settings_window), Some((custom_enabled, cursor_enabled))) =
1441            (&mut self.settings_window, shader_states)
1442        {
1443            settings_window.sync_shader_states(custom_enabled, cursor_enabled);
1444        }
1445
1446        // Update settings window with new terminal dimensions after resize
1447        if let (Some(settings_window), Some((cols, rows))) = (&mut self.settings_window, grid_size)
1448        {
1449            settings_window.settings_ui.update_current_size(cols, rows);
1450        }
1451
1452        // Close window if requested
1453        if should_close {
1454            self.close_window(window_id);
1455        }
1456
1457        // Exit if no windows remain
1458        if self.should_exit {
1459            event_loop.exit();
1460        }
1461    }
1462
1463    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1464        // Check CLI timing-based options (exit-after, screenshot, command)
1465        self.check_cli_timers();
1466
1467        // Check for updates (respects configured frequency)
1468        self.check_for_updates();
1469
1470        // Process menu events
1471        // Find the actually focused window (the one with is_focused == true)
1472        let focused_window = self.get_focused_window_id();
1473        self.process_menu_events(event_loop, focused_window);
1474
1475        // Check if any window requested opening the settings window
1476        // Also collect shader reload results for propagation to standalone settings window
1477        let mut open_settings = false;
1478        let mut open_settings_profiles_tab = false;
1479        let mut background_shader_result: Option<Option<String>> = None;
1480        let mut cursor_shader_result: Option<Option<String>> = None;
1481        let mut profiles_to_update: Option<Vec<crate::profile::Profile>> = None;
1482        let mut arrangement_restore_name: Option<String> = None;
1483        let mut reload_dynamic_profiles = false;
1484        let mut config_changed_by_agent = false;
1485
1486        for window_state in self.windows.values_mut() {
1487            if window_state.open_settings_window_requested {
1488                window_state.open_settings_window_requested = false;
1489                open_settings = true;
1490            }
1491            if window_state.open_settings_profiles_tab {
1492                window_state.open_settings_profiles_tab = false;
1493                open_settings_profiles_tab = true;
1494            }
1495
1496            // Check for arrangement restore request from keybinding
1497            if let Some(name) = window_state.pending_arrangement_restore.take() {
1498                arrangement_restore_name = Some(name);
1499            }
1500
1501            // Check for dynamic profile reload request from keybinding
1502            if window_state.reload_dynamic_profiles_requested {
1503                window_state.reload_dynamic_profiles_requested = false;
1504                reload_dynamic_profiles = true;
1505            }
1506
1507            // Check if profiles menu needs updating (from profile modal save)
1508            if window_state.profiles_menu_needs_update {
1509                window_state.profiles_menu_needs_update = false;
1510                // Get a copy of the profiles for menu update
1511                profiles_to_update = Some(window_state.profile_manager.to_vec());
1512            }
1513
1514            window_state.about_to_wait(event_loop);
1515
1516            // If an agent/MCP config update was applied, sync to WindowManager's
1517            // config so that subsequent saves (update checker, settings) don't
1518            // overwrite the agent's changes.
1519            if window_state.config_changed_by_agent {
1520                window_state.config_changed_by_agent = false;
1521                config_changed_by_agent = true;
1522            }
1523
1524            // Collect shader reload results and clear them from window_state
1525            if let Some(result) = window_state.background_shader_reload_result.take() {
1526                background_shader_result = Some(result);
1527            }
1528            if let Some(result) = window_state.cursor_shader_reload_result.take() {
1529                cursor_shader_result = Some(result);
1530            }
1531        }
1532
1533        // Sync agent config changes to WindowManager and settings window
1534        // so other saves (update checker, settings) don't overwrite the agent's changes
1535        if config_changed_by_agent && let Some(window_state) = self.windows.values().next() {
1536            log::info!("CONFIG: syncing agent config changes to WindowManager");
1537            self.config = window_state.config.clone();
1538            // Force-update the settings window's config copy so it doesn't
1539            // send stale values back via ApplyConfig/SaveConfig.
1540            // Must use force_update_config to bypass the has_changes guard.
1541            if let Some(settings_window) = &mut self.settings_window {
1542                settings_window.force_update_config(self.config.clone());
1543            }
1544        }
1545
1546        // Check for dynamic profile updates
1547        while let Some(update) = self.dynamic_profile_manager.try_recv() {
1548            self.dynamic_profile_manager.update_status(&update);
1549
1550            // Merge into all window profile managers
1551            for window_state in self.windows.values_mut() {
1552                crate::profile::dynamic::merge_dynamic_profiles(
1553                    &mut window_state.profile_manager,
1554                    &update.profiles,
1555                    &update.url,
1556                    &update.conflict_resolution,
1557                );
1558                window_state.profiles_menu_needs_update = true;
1559            }
1560
1561            log::info!(
1562                "Dynamic profiles updated from {}: {} profiles{}",
1563                update.url,
1564                update.profiles.len(),
1565                update
1566                    .error
1567                    .as_ref()
1568                    .map_or(String::new(), |e| format!(" (error: {e})"))
1569            );
1570
1571            // Ensure profiles_to_update is refreshed after dynamic merge
1572            if let Some(window_state) = self.windows.values().next() {
1573                profiles_to_update = Some(window_state.profile_manager.to_vec());
1574            }
1575        }
1576
1577        // Trigger dynamic profile refresh if requested via keybinding
1578        if reload_dynamic_profiles {
1579            self.dynamic_profile_manager
1580                .refresh_all(&self.config.dynamic_profile_sources, &self.runtime);
1581        }
1582
1583        // Update profiles menu if profiles changed
1584        if let Some(profiles) = profiles_to_update
1585            && let Some(menu) = &mut self.menu
1586        {
1587            let profile_refs: Vec<&crate::profile::Profile> = profiles.iter().collect();
1588            menu.update_profiles(&profile_refs);
1589        }
1590
1591        // Open settings window if requested (F12 or Cmd+,)
1592        if open_settings {
1593            self.open_settings_window(event_loop);
1594        }
1595
1596        // Navigate to Profiles tab if requested (from drawer "Manage" button)
1597        if open_settings_profiles_tab && let Some(sw) = &mut self.settings_window {
1598            sw.settings_ui
1599                .set_selected_tab(crate::settings_ui::sidebar::SettingsTab::Profiles);
1600        }
1601
1602        // Restore arrangement if requested via keybinding
1603        if let Some(name) = arrangement_restore_name {
1604            self.restore_arrangement_by_name(&name, event_loop);
1605        }
1606
1607        // Propagate shader reload results to standalone settings window
1608        if let Some(settings_window) = &mut self.settings_window {
1609            if let Some(result) = background_shader_result {
1610                match result {
1611                    Some(err) => settings_window.set_shader_error(Some(err)),
1612                    None => settings_window.clear_shader_error(),
1613                }
1614            }
1615            if let Some(result) = cursor_shader_result {
1616                match result {
1617                    Some(err) => settings_window.set_cursor_shader_error(Some(err)),
1618                    None => settings_window.clear_cursor_shader_error(),
1619                }
1620            }
1621        }
1622
1623        // Close any windows that have is_shutting_down set
1624        // This handles deferred closes from quit confirmation, tab bar close, and shell exit
1625        let shutting_down: Vec<_> = self
1626            .windows
1627            .iter()
1628            .filter(|(_, ws)| ws.is_shutting_down)
1629            .map(|(id, _)| *id)
1630            .collect();
1631
1632        for window_id in shutting_down {
1633            self.close_window(window_id);
1634        }
1635
1636        // Sync coprocess and script running state to settings window
1637        if self.settings_window.is_some() {
1638            self.sync_coprocess_running_state();
1639            self.sync_script_running_state();
1640        }
1641
1642        // Request redraw for settings window if it needs continuous updates
1643        self.request_settings_redraw();
1644
1645        // Exit if no windows remain
1646        if self.should_exit {
1647            event_loop.exit();
1648        }
1649    }
1650}