1use 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 pub(crate) fn check_notifications(&mut self) {
16 let tab = if let Some(t) = self.tab_manager.active_tab() {
17 t
18 } else {
19 return;
20 };
21
22 if let Ok(term) = tab.terminal.try_lock() {
23 if term.has_notifications() {
25 let notifications = term.take_notifications();
26 for notif in notifications {
27 self.deliver_notification(¬if.title, ¬if.message);
28 }
29 }
30 }
31 }
32
33 pub(crate) fn check_bell(&mut self) {
34 if self.config.notification_bell_sound == 0
36 && !self.config.notification_bell_visual
37 && !self.config.notification_bell_desktop
38 {
39 return;
40 }
41
42 let (current_bell_count, last_count) = {
44 let tab = if let Some(t) = self.tab_manager.active_tab() {
45 t
46 } else {
47 return;
48 };
49
50 if let Ok(term) = tab.terminal.try_lock() {
51 (term.bell_count(), tab.bell.last_count)
52 } else {
53 return;
54 }
55 };
56
57 if current_bell_count > last_count {
58 let bell_events = current_bell_count - last_count;
60 log::info!("Bell event detected ({} bell(s))", bell_events);
61 log::info!(
62 " Config: sound={}, visual={}, desktop={}",
63 self.config.notification_bell_sound,
64 self.config.notification_bell_visual,
65 self.config.notification_bell_desktop
66 );
67
68 if self.config.notification_bell_sound > 0 {
70 if let Some(tab) = self.tab_manager.active_tab() {
71 if let Some(ref audio_bell) = tab.bell.audio {
72 log::info!(
73 " Playing audio bell at {}% volume",
74 self.config.notification_bell_sound
75 );
76 audio_bell.play(self.config.notification_bell_sound);
77 } else {
78 log::warn!(" Audio bell requested but not initialized");
79 }
80 }
81 } else {
82 log::debug!(" Audio bell disabled (volume=0)");
83 }
84
85 if self.config.notification_bell_visual {
87 log::info!(" Triggering visual bell flash");
88 if let Some(tab) = self.tab_manager.active_tab_mut() {
89 tab.bell.visual_flash = Some(std::time::Instant::now());
90 }
91 if let Some(window) = &self.window {
93 window.request_redraw();
94 }
95 } else {
96 log::debug!(" Visual bell disabled");
97 }
98
99 if self.config.notification_bell_desktop {
101 log::info!(" Sending desktop notification");
102 let message = if bell_events == 1 {
103 "Terminal bell".to_string()
104 } else {
105 format!("Terminal bell ({} events)", bell_events)
106 };
107 self.deliver_notification("Terminal", &message);
108 } else {
109 log::debug!(" Desktop notification disabled");
110 }
111
112 if let Some(tab) = self.tab_manager.active_tab_mut() {
114 tab.bell.last_count = current_bell_count;
115 }
116 }
117 }
118
119 #[allow(dead_code)]
120 fn take_screenshot(&self) {
121 log::info!("Taking screenshot...");
122
123 let terminal = if let Some(tab) = self.tab_manager.active_tab() {
124 Arc::clone(&tab.terminal)
125 } else {
126 log::warn!("No terminal available for screenshot");
127 self.deliver_notification("Screenshot Error", "No terminal available");
128 return;
129 };
130
131 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
133 let format = &self.config.screenshot_format;
134 let filename = format!("par-term_screenshot_{}.{}", timestamp, format);
135
136 if let Some(home_dir) = dirs::home_dir() {
138 let screenshot_dir = home_dir.join("par-term-screenshots");
139 if !screenshot_dir.exists()
140 && let Err(e) = std::fs::create_dir_all(&screenshot_dir)
141 {
142 log::error!("Failed to create screenshot directory: {}", e);
143 self.deliver_notification(
144 "Screenshot Error",
145 &format!("Failed to create directory: {}", e),
146 );
147 return;
148 }
149
150 let path = screenshot_dir.join(&filename);
151 let path_str = path.to_string_lossy().to_string();
152
153 let terminal_clone = terminal;
155 let format_clone = format.clone();
156
157 let result = std::thread::spawn(move || {
159 if let Ok(term) = terminal_clone.try_lock() {
160 term.screenshot_to_file(&path, &format_clone, 0)
162 } else {
163 Err(anyhow::anyhow!("Failed to lock terminal"))
164 }
165 })
166 .join();
167
168 match result {
169 Ok(Ok(())) => {
170 log::info!("Screenshot saved to: {}", path_str);
171 self.deliver_notification(
172 "Screenshot Saved",
173 &format!("Saved to: {}", path_str),
174 );
175 }
176 Ok(Err(e)) => {
177 log::error!("Failed to save screenshot: {}", e);
178 self.deliver_notification(
179 "Screenshot Error",
180 &format!("Failed to save: {}", e),
181 );
182 }
183 Err(e) => {
184 log::error!("Screenshot thread panicked: {:?}", e);
185 self.deliver_notification("Screenshot Error", "Screenshot thread failed");
186 }
187 }
188 } else {
189 log::error!("Failed to get home directory");
190 self.deliver_notification("Screenshot Error", "Failed to get home directory");
191 }
192 }
193
194 #[allow(dead_code)]
197 fn toggle_recording(&mut self) {
198 self.deliver_notification(
199 "Recording Not Available",
200 "Recording APIs are not yet implemented in the core library",
201 );
202 }
203
204 pub(crate) fn deliver_notification(&self, title: &str, message: &str) {
205 if !title.is_empty() {
207 log::info!("=== Notification: {} ===", title);
208 log::info!("{}", message);
209 log::info!("===========================");
210 } else {
211 log::info!("=== Notification ===");
212 log::info!("{}", message);
213 log::info!("===================");
214 }
215
216 #[cfg(not(target_os = "macos"))]
218 {
219 use notify_rust::Notification;
220 let notification_title = if !title.is_empty() {
221 title
222 } else {
223 "Terminal Notification"
224 };
225
226 if let Err(e) = Notification::new()
227 .summary(notification_title)
228 .body(message)
229 .timeout(notify_rust::Timeout::Milliseconds(3000))
230 .show()
231 {
232 log::warn!("Failed to send desktop notification: {}", e);
233 }
234 }
235
236 #[cfg(target_os = "macos")]
237 {
238 let notification_title = if !title.is_empty() {
240 title
241 } else {
242 "Terminal Notification"
243 };
244
245 let escaped_title = notification_title.replace('"', "\\\"");
247 let escaped_message = message.replace('"', "\\\"");
248
249 let script = format!(
251 r#"display notification "{}" with title "{}""#,
252 escaped_message, escaped_title
253 );
254
255 if let Err(e) = std::process::Command::new("osascript")
256 .arg("-e")
257 .arg(&script)
258 .output()
259 {
260 log::warn!("Failed to send macOS desktop notification: {}", e);
261 }
262 }
263 }
264
265 pub(crate) fn update_window_title_with_shell_integration(&self) {
268 let tab = if let Some(t) = self.tab_manager.active_tab() {
270 t
271 } else {
272 return;
273 };
274
275 if tab.scroll_state.offset != 0 {
277 return;
278 }
279
280 if tab.mouse.hovered_url.is_some() {
282 return;
283 }
284
285 let window = if let Some(w) = &self.window {
287 w
288 } else {
289 return;
290 };
291
292 if let Ok(term) = tab.terminal.try_lock() {
294 let mut title_parts = vec![self.config.window_title.clone()];
295
296 if let Some(cwd) = term.shell_integration_cwd() {
298 let abbreviated_cwd = if let Some(home) = dirs::home_dir() {
300 cwd.replace(&home.to_string_lossy().to_string(), "~")
301 } else {
302 cwd
303 };
304 title_parts.push(format!("({})", abbreviated_cwd));
305 }
306
307 if let Some(exit_code) = term.shell_integration_exit_code()
309 && exit_code != 0
310 {
311 title_parts.push(format!("[Exit: {}]", exit_code));
312 }
313
314 if self.is_recording {
316 title_parts.push("[RECORDING]".to_string());
317 }
318
319 let title = title_parts.join(" ");
321 window.set_title(&title);
322 }
323 }
324
325 pub(crate) fn handle_window_event(
327 &mut self,
328 event_loop: &ActiveEventLoop,
329 event: WindowEvent,
330 ) -> bool {
331 use winit::keyboard::{Key, NamedKey};
332
333 if let WindowEvent::KeyboardInput {
335 event: key_event, ..
336 } = &event
337 {
338 match &key_event.logical_key {
339 Key::Character(s) => {
340 log::trace!(
341 "window_event: Character '{}', state={:?}",
342 s,
343 key_event.state
344 );
345 }
346 Key::Named(NamedKey::Space) => {
347 log::debug!("SPACE EVENT: state={:?}", key_event.state);
348 }
349 Key::Named(named) => {
350 log::trace!(
351 "window_event: Named key {:?}, state={:?}",
352 named,
353 key_event.state
354 );
355 }
356 other => {
357 log::trace!(
358 "window_event: Other key {:?}, state={:?}",
359 other,
360 key_event.state
361 );
362 }
363 }
364 }
365
366 let egui_consumed =
368 if let (Some(egui_state), Some(window)) = (&mut self.egui_state, &self.window) {
369 let event_response = egui_state.on_window_event(window, &event);
370 event_response.consumed
371 } else {
372 false
373 };
374
375 if egui_consumed
377 && !self.settings_ui.visible
378 && let WindowEvent::KeyboardInput {
379 event: key_event, ..
380 } = &event
381 && let Key::Named(NamedKey::Space) = &key_event.logical_key
382 {
383 log::debug!("egui tried to consume Space (UI closed, ignoring)");
384 }
385
386 let any_ui_visible =
389 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
390 if egui_consumed
391 && any_ui_visible
392 && !matches!(
393 event,
394 WindowEvent::CloseRequested | WindowEvent::RedrawRequested
395 )
396 {
397 if let WindowEvent::KeyboardInput {
398 event: key_event, ..
399 } = &event
400 {
401 match &key_event.logical_key {
402 Key::Named(NamedKey::Space) => {
403 log::debug!("egui consumed Space while UI panel is visible")
404 }
405 Key::Named(_) => {
406 log::debug!("egui consumed named key while UI panel is visible")
407 }
408 _ => {}
409 }
410 }
411 return false; }
413
414 match event {
415 WindowEvent::CloseRequested => {
416 log::info!("Close requested for window");
417 self.is_shutting_down = true;
419 for tab in self.tab_manager.tabs_mut() {
421 if let Some(task) = tab.refresh_task.take() {
422 task.abort();
423 }
424 }
425 log::info!("Refresh tasks aborted");
426 return true; }
428
429 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
430 if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
431 log::info!(
432 "Scale factor changed to {} (display change detected)",
433 scale_factor
434 );
435
436 let size = window.inner_size();
437 let (cols, rows) = renderer.handle_scale_factor_change(scale_factor, size);
438
439 renderer.reconfigure_surface();
442
443 let cell_width = renderer.cell_width();
445 let cell_height = renderer.cell_height();
446 let width_px = (cols as f32 * cell_width) as usize;
447 let height_px = (rows as f32 * cell_height) as usize;
448
449 for tab in self.tab_manager.tabs_mut() {
451 if let Ok(mut term) = tab.terminal.try_lock() {
452 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
453 }
454 }
455
456 #[cfg(target_os = "macos")]
458 {
459 if let Err(e) =
460 crate::macos_metal::configure_metal_layer_for_performance(window)
461 {
462 log::warn!(
463 "Failed to reconfigure Metal layer after display change: {}",
464 e
465 );
466 }
467 }
468
469 window.request_redraw();
471 }
472 }
473
474 WindowEvent::Moved(_) => {
476 if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
477 log::debug!(
478 "Window moved - reconfiguring surface for potential display change"
479 );
480
481 renderer.reconfigure_surface();
484
485 #[cfg(target_os = "macos")]
487 {
488 if let Err(e) =
489 crate::macos_metal::configure_metal_layer_for_performance(window)
490 {
491 log::warn!(
492 "Failed to reconfigure Metal layer after window move: {}",
493 e
494 );
495 }
496 }
497
498 window.request_redraw();
500 }
501 }
502
503 WindowEvent::Resized(physical_size) => {
504 if let Some(renderer) = &mut self.renderer {
505 let (cols, rows) = renderer.resize(physical_size);
506
507 let cell_width = renderer.cell_width();
509 let cell_height = renderer.cell_height();
510 let width_px = (cols as f32 * cell_width) as usize;
511 let height_px = (rows as f32 * cell_height) as usize;
512
513 for tab in self.tab_manager.tabs_mut() {
518 if let Ok(mut term) = tab.terminal.try_lock() {
519 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
520 tab.cache.scrollback_len = term.scrollback_len();
521 }
522 tab.cache.cells = None;
524 }
525
526 if let Some(tab) = self.tab_manager.active_tab() {
528 let total_lines = rows + tab.cache.scrollback_len;
529 renderer.update_scrollbar(tab.scroll_state.offset, rows, total_lines);
530 }
531 }
532 }
533
534 WindowEvent::KeyboardInput { event, .. } => {
535 self.handle_key_event(event, event_loop);
536 }
537
538 WindowEvent::ModifiersChanged(modifiers) => {
539 self.input_handler.update_modifiers(modifiers);
540 }
541
542 WindowEvent::MouseWheel { delta, .. } => {
543 if !self.is_egui_using_pointer() {
545 self.handle_mouse_wheel(delta);
546 }
547 }
548
549 WindowEvent::MouseInput { button, state, .. } => {
550 if !self.is_egui_using_pointer() {
552 self.handle_mouse_button(button, state);
553 }
554 }
555
556 WindowEvent::CursorMoved { position, .. } => {
557 if !self.is_egui_using_pointer() {
559 self.handle_mouse_move((position.x, position.y));
560 }
561 }
562
563 WindowEvent::RedrawRequested => {
564 if self.is_shutting_down {
566 return false;
567 }
568
569 if self.config.exit_on_shell_exit {
571 let (shell_exited, active_tab_id, tab_count) = {
573 let exited = self.tab_manager.active_tab().is_some_and(|tab| {
574 tab.terminal
575 .try_lock()
576 .ok()
577 .is_some_and(|term| !term.is_running())
578 });
579 let tab_id = self.tab_manager.active_tab_id();
580 let count = self.tab_manager.tab_count();
581 (exited, tab_id, count)
582 };
583
584 if shell_exited {
585 log::info!("Shell in active tab has exited");
586 if tab_count <= 1 {
587 log::info!("Last tab, closing window");
589 self.is_shutting_down = true;
590 for tab in self.tab_manager.tabs_mut() {
591 tab.stop_refresh_task();
592 }
593 return true;
594 } else if let Some(tab_id) = active_tab_id {
595 let _ = self.tab_manager.close_tab(tab_id);
597 }
598 }
599 }
600
601 self.render();
602 }
603
604 _ => {}
605 }
606
607 false }
609
610 pub(crate) fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
612 if self.is_shutting_down {
614 return;
615 }
616
617 self.check_notifications();
619
620 self.check_bell();
622
623 self.update_window_title_with_shell_integration();
625
626 let now = std::time::Instant::now();
632 let mut next_wake = now + std::time::Duration::from_secs(1); if self.config.cursor_blink {
637 if self.cursor_blink_timer.is_none() {
638 let blink_interval =
639 std::time::Duration::from_millis(self.config.cursor_blink_interval);
640 self.cursor_blink_timer = Some(now + blink_interval);
641 }
642
643 if let Some(next_blink) = self.cursor_blink_timer {
644 if now >= next_blink {
645 self.needs_redraw = true;
647 let blink_interval =
648 std::time::Duration::from_millis(self.config.cursor_blink_interval);
649 self.cursor_blink_timer = Some(now + blink_interval);
650 } else if next_blink < next_wake {
651 next_wake = next_blink;
653 }
654 }
655 }
656
657 if let Some(tab) = self.tab_manager.active_tab() {
660 if tab.scroll_state.animation_start.is_some() {
661 self.needs_redraw = true;
662 let next_frame = now + std::time::Duration::from_millis(16);
663 if next_frame < next_wake {
664 next_wake = next_frame;
665 }
666 }
667
668 if tab.bell.visual_flash.is_some() {
671 self.needs_redraw = true;
672 let next_frame = now + std::time::Duration::from_millis(16);
673 if next_frame < next_wake {
674 next_wake = next_frame;
675 }
676 }
677
678 if (tab.mouse.is_selecting
681 || tab.mouse.selection.is_some()
682 || tab.scroll_state.dragging)
683 && tab.mouse.button_pressed
684 {
685 self.needs_redraw = true;
686 }
687 }
688
689 if let Some(renderer) = &self.renderer
692 && renderer.needs_continuous_render()
693 {
694 self.needs_redraw = true;
695 let next_frame = now + std::time::Duration::from_millis(16);
696 if next_frame < next_wake {
697 next_wake = next_frame;
698 }
699 }
700
701 if self.needs_redraw
704 && let Some(window) = &self.window
705 {
706 window.request_redraw();
707 self.needs_redraw = false;
708 }
709
710 event_loop.set_control_flow(ControlFlow::WaitUntil(next_wake));
712 }
713}
714
715impl ApplicationHandler for WindowManager {
716 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
717 if self.windows.is_empty() {
719 self.create_window(event_loop);
720 }
721 }
722
723 fn window_event(
724 &mut self,
725 event_loop: &ActiveEventLoop,
726 window_id: WindowId,
727 event: WindowEvent,
728 ) {
729 let should_close = if let Some(window_state) = self.windows.get_mut(&window_id) {
731 window_state.handle_window_event(event_loop, event)
732 } else {
733 false
734 };
735
736 if should_close {
738 self.close_window(window_id);
739 }
740
741 if self.should_exit {
743 event_loop.exit();
744 }
745 }
746
747 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
748 let focused_window = self.windows.keys().next().copied();
751 self.process_menu_events(event_loop, focused_window);
752
753 for window_state in self.windows.values_mut() {
755 window_state.about_to_wait(event_loop);
756 }
757
758 if self.should_exit {
760 event_loop.exit();
761 }
762 }
763}