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