1use crate::cell_renderer::Cell;
2use crate::clipboard_history_ui::{ClipboardHistoryAction, ClipboardHistoryUI};
3use crate::config::Config;
4use crate::help_ui::HelpUI;
5use crate::input::InputHandler;
6use crate::renderer::Renderer;
7use crate::scroll_state::ScrollState;
8use crate::selection::{Selection, SelectionMode};
9use crate::settings_ui::{SettingsUI, ShaderEditorResult};
10use crate::terminal::{ClipboardSlot, TerminalManager};
11use crate::url_detection;
12use anyhow::Result;
13use std::sync::Arc;
14use tokio::runtime::Runtime;
15use tokio::sync::Mutex;
16use tokio::task::JoinHandle;
17use wgpu::SurfaceError;
18use winit::application::ApplicationHandler;
19use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
20use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
21use winit::window::{Window, WindowId};
22
23pub struct App {
25 config: Config,
26 runtime: Arc<Runtime>,
27}
28
29impl App {
30 pub fn new(runtime: Arc<Runtime>) -> Result<Self> {
32 let config = Config::load()?;
33 Ok(Self { config, runtime })
34 }
35
36 pub fn run(self) -> Result<()> {
38 let event_loop = EventLoop::new()?;
39 event_loop.set_control_flow(ControlFlow::Wait);
42
43 let mut app_state = AppState::new(self.config, self.runtime);
44
45 event_loop.run_app(&mut app_state)?;
46
47 Ok(())
48 }
49}
50
51struct AppState {
53 config: Config,
54 window: Option<Arc<Window>>,
55 renderer: Option<Renderer>,
56 terminal: Option<Arc<Mutex<TerminalManager>>>,
57 input_handler: InputHandler,
58 refresh_task: Option<JoinHandle<()>>,
59 runtime: Arc<Runtime>,
60 scroll_state: ScrollState,
61
62 selection: Option<Selection>, is_selecting: bool, mouse_position: (f64, f64), cached_scrollback_len: usize, mouse_button_pressed: bool, last_click_time: Option<std::time::Instant>, click_count: u32, click_position: Option<(usize, usize)>, detected_urls: Vec<url_detection::DetectedUrl>, hovered_url: Option<String>, cursor_opacity: f32, last_cursor_blink: Option<std::time::Instant>, last_key_press: Option<std::time::Instant>, is_fullscreen: bool, audio_bell: Option<crate::audio_bell::AudioBell>, last_bell_count: u64, visual_bell_flash: Option<std::time::Instant>, egui_ctx: Option<egui::Context>, egui_state: Option<egui_winit::State>, settings_ui: SettingsUI, help_ui: HelpUI, clipboard_history_ui: ClipboardHistoryUI, is_recording: bool, #[allow(dead_code)] recording_start_time: Option<std::time::Instant>, is_shutting_down: bool, cached_cells: Option<Vec<Cell>>, last_generation: u64, last_scroll_offset: usize, last_cursor_pos: Option<(usize, usize)>, last_selection: Option<Selection>, last_applied_opacity: f32, needs_redraw: bool, cursor_blink_timer: Option<std::time::Instant>, debug_frame_times: Vec<std::time::Duration>, debug_cell_gen_time: std::time::Duration, debug_render_time: std::time::Duration, debug_cache_hit: bool, debug_last_frame_start: Option<std::time::Instant>, show_fps_overlay: bool, fps_value: f64, pending_font_rebuild: bool, cached_terminal_title: String, }
111
112impl AppState {
113 fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
115 let mut extracted = String::new();
116 let end_bound = end_col.unwrap_or(usize::MAX);
117
118 if start_col > end_bound {
119 return extracted;
120 }
121
122 for (idx, ch) in line.chars().enumerate() {
123 if idx > end_bound {
124 break;
125 }
126
127 if idx >= start_col {
128 extracted.push(ch);
129 }
130 }
131
132 extracted
133 }
134
135 fn new(config: Config, runtime: Arc<Runtime>) -> Self {
136 let initial_opacity = config.window_opacity;
137 let settings_ui = SettingsUI::new(config.clone());
138
139 Self {
140 config,
141 window: None,
142 renderer: None,
143 terminal: None,
144 input_handler: InputHandler::new(),
145 refresh_task: None,
146 runtime,
147 scroll_state: ScrollState::new(),
148
149 selection: None,
150 is_selecting: false,
151
152 mouse_position: (0.0, 0.0),
153 cached_scrollback_len: 0,
154 mouse_button_pressed: false,
155 last_click_time: None,
156 click_count: 0,
157 click_position: None,
158 detected_urls: Vec::new(),
159 hovered_url: None,
160 cursor_opacity: 1.0,
161 last_cursor_blink: None,
162 last_key_press: None,
163 is_fullscreen: false,
164 audio_bell: {
165 match crate::audio_bell::AudioBell::new() {
166 Ok(bell) => {
167 log::info!("Audio bell initialized successfully");
168 Some(bell)
169 }
170 Err(e) => {
171 log::warn!("Failed to initialize audio bell: {}", e);
172 None
173 }
174 }
175 },
176 last_bell_count: 0,
177 visual_bell_flash: None,
178 egui_ctx: None,
179 egui_state: None,
180 settings_ui,
181 help_ui: HelpUI::new(),
182 clipboard_history_ui: ClipboardHistoryUI::new(),
183 is_recording: false,
184 recording_start_time: None,
185 is_shutting_down: false,
186 cached_cells: None,
187 last_generation: 0,
188 last_scroll_offset: 0,
189 last_cursor_pos: None,
190 last_selection: None,
191 last_applied_opacity: initial_opacity,
192
193 needs_redraw: true,
194 cursor_blink_timer: None,
195 debug_frame_times: Vec::with_capacity(60),
196 debug_cell_gen_time: std::time::Duration::ZERO,
197 debug_render_time: std::time::Duration::ZERO,
198 debug_cache_hit: false,
199 debug_last_frame_start: None,
200 show_fps_overlay: false,
201 fps_value: 0.0,
202 pending_font_rebuild: false,
203 cached_terminal_title: String::new(),
204 }
205 }
206
207 fn rebuild_renderer(&mut self) -> Result<()> {
209 let window = if let Some(w) = &self.window {
210 Arc::clone(w)
211 } else {
212 return Ok(()); };
214
215 let theme = self.config.load_theme();
216 let font_family = if self.config.font_family.is_empty() {
217 None
218 } else {
219 Some(self.config.font_family.as_str())
220 };
221
222 let renderer = self.runtime.block_on(Renderer::new(
223 Arc::clone(&window),
224 font_family,
225 self.config.font_family_bold.as_deref(),
226 self.config.font_family_italic.as_deref(),
227 self.config.font_family_bold_italic.as_deref(),
228 &self.config.font_ranges,
229 self.config.font_size,
230 self.config.window_padding,
231 self.config.line_spacing,
232 self.config.char_spacing,
233 &self.config.scrollbar_position,
234 self.config.scrollbar_width,
235 self.config.scrollbar_thumb_color,
236 self.config.scrollbar_track_color,
237 self.config.enable_text_shaping,
238 self.config.enable_ligatures,
239 self.config.enable_kerning,
240 self.config.vsync_mode,
241 self.config.window_opacity,
242 theme.background.as_array(),
243 self.config.background_image.as_deref(),
244 self.config.background_image_enabled,
245 self.config.background_image_mode,
246 self.config.background_image_opacity,
247 self.config.custom_shader.as_deref(),
248 self.config.custom_shader_enabled,
249 self.config.custom_shader_animation,
250 self.config.custom_shader_animation_speed,
251 self.config.custom_shader_text_opacity,
252 self.config.custom_shader_full_content,
253 ))?;
254
255 let (cols, rows) = renderer.grid_size();
256 let cell_width = renderer.cell_width();
257 let cell_height = renderer.cell_height();
258 let width_px = (cols as f32 * cell_width) as usize;
259 let height_px = (rows as f32 * cell_height) as usize;
260
261 if let Some(terminal) = &self.terminal
262 && let Ok(mut term) = terminal.try_lock()
263 {
264 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
265 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
266 term.set_theme(self.config.load_theme());
267 }
268
269 self.renderer = Some(renderer);
270 self.cached_cells = None;
271 self.needs_redraw = true;
272
273 let previous_memory = self
278 .egui_ctx
279 .as_ref()
280 .map(|ctx| ctx.memory(|mem| mem.clone()));
281
282 let scale_factor = window.scale_factor() as f32;
283 let egui_ctx = egui::Context::default();
284 if let Some(memory) = previous_memory {
285 egui_ctx.memory_mut(|mem| *mem = memory);
286 }
287 let egui_state = egui_winit::State::new(
288 egui_ctx.clone(),
289 egui::ViewportId::ROOT,
290 &window,
291 Some(scale_factor),
292 None,
293 None,
294 );
295 self.egui_ctx = Some(egui_ctx);
296 self.egui_state = Some(egui_state);
297
298 if let Some(window) = &self.window {
299 window.request_redraw();
300 }
301
302 Ok(())
303 }
304
305 async fn initialize_async(&mut self, window: Window) -> Result<()> {
306 window.set_ime_allowed(true);
308 log::debug!("IME enabled for character input");
309
310 let window = Arc::new(window);
311
312 let egui_ctx = egui::Context::default();
314 let egui_state = egui_winit::State::new(
315 egui_ctx.clone(),
316 egui::ViewportId::ROOT,
317 &window,
318 Some(window.scale_factor() as f32),
319 None,
320 None, );
322 self.egui_ctx = Some(egui_ctx);
323 self.egui_state = Some(egui_state);
324
325 let font_family = if self.config.font_family.is_empty() {
327 None
328 } else {
329 Some(self.config.font_family.as_str())
330 };
331 let theme = self.config.load_theme();
332 let renderer = Renderer::new(
333 Arc::clone(&window),
334 font_family,
335 self.config.font_family_bold.as_deref(),
336 self.config.font_family_italic.as_deref(),
337 self.config.font_family_bold_italic.as_deref(),
338 &self.config.font_ranges,
339 self.config.font_size,
340 self.config.window_padding,
341 self.config.line_spacing,
342 self.config.char_spacing,
343 &self.config.scrollbar_position,
344 self.config.scrollbar_width,
345 self.config.scrollbar_thumb_color,
346 self.config.scrollbar_track_color,
347 self.config.enable_text_shaping,
348 self.config.enable_ligatures,
349 self.config.enable_kerning,
350 self.config.vsync_mode,
351 self.config.window_opacity,
352 theme.background.as_array(),
353 self.config.background_image.as_deref(),
354 self.config.background_image_enabled,
355 self.config.background_image_mode,
356 self.config.background_image_opacity,
357 self.config.custom_shader.as_deref(),
358 self.config.custom_shader_enabled,
359 self.config.custom_shader_animation,
360 self.config.custom_shader_animation_speed,
361 self.config.custom_shader_text_opacity,
362 self.config.custom_shader_full_content,
363 )
364 .await?;
365
366 #[cfg(target_os = "macos")]
371 {
372 if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
373 log::warn!("Failed to configure Metal layer: {}", e);
374 log::warn!(
375 "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
376 );
377 }
378 if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
380 log::warn!("Failed to set initial Metal layer opacity: {}", e);
381 }
382 }
383
384 let mut terminal = TerminalManager::new_with_scrollback(
386 self.config.cols,
387 self.config.rows,
388 self.config.scrollback_lines,
389 )?;
390
391 terminal.set_theme(self.config.load_theme());
393
394 terminal.set_max_clipboard_sync_events(self.config.clipboard_max_sync_events);
396 terminal.set_max_clipboard_event_bytes(self.config.clipboard_max_event_bytes);
397
398 let (renderer_cols, renderer_rows) = renderer.grid_size();
400 log::info!(
401 "Initial terminal dimensions: {}x{}",
402 renderer_cols,
403 renderer_rows
404 );
405
406 let cell_width = renderer.cell_width();
409 let cell_height = renderer.cell_height();
410 let width_px = (renderer_cols as f32 * cell_width) as usize;
411 let height_px = (renderer_rows as f32 * cell_height) as usize;
412 terminal.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px)?;
413 log::info!(
414 "Initial terminal pixel dimensions: {}x{} px",
415 width_px,
416 height_px
417 );
418
419 let working_dir = self.config.working_directory.as_deref();
421 let shell_env = self.config.shell_env.as_ref();
422
423 let (shell_cmd, shell_args) = if let Some(ref custom) = self.config.custom_shell {
425 (custom.clone(), self.config.shell_args.clone())
426 } else {
427 #[cfg(target_os = "windows")]
428 {
429 ("powershell.exe".to_string(), None)
430 }
431 #[cfg(not(target_os = "windows"))]
432 {
433 (
434 std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()),
435 None,
436 )
437 }
438 };
439
440 let shell_args_deref = shell_args.as_deref();
441 terminal.spawn_custom_shell_with_dir(
442 &shell_cmd,
443 shell_args_deref,
444 working_dir,
445 shell_env,
446 )?;
447
448 let cell_width = renderer.cell_width() as u32;
450 let cell_height = renderer.cell_height() as u32;
451 log::info!("Setting cell dimensions: {}x{}", cell_width, cell_height);
452 terminal.set_cell_dimensions(cell_width, cell_height);
453
454 self.window = Some(Arc::clone(&window));
455 self.renderer = Some(renderer);
456 self.terminal = Some(Arc::new(Mutex::new(terminal)));
457
458 let window_clone = Arc::clone(&window);
460 let terminal_clone = Arc::clone(self.terminal.as_ref().unwrap());
461 let max_fps = self.config.max_fps.max(1);
462 let refresh_interval_ms = 1000 / max_fps;
463
464 let handle = self.runtime.spawn(async move {
465 let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(
466 refresh_interval_ms as u64,
467 ));
468 let mut last_gen = 0;
470
471 loop {
472 interval.tick().await;
473
474 let should_redraw = if let Ok(term) = terminal_clone.try_lock() {
477 let current_gen = term.update_generation();
478 if current_gen > last_gen {
479 last_gen = current_gen;
480 true
481 } else {
482 term.has_updates()
484 }
485 } else {
486 false
488 };
489
490 if should_redraw {
491 window_clone.request_redraw();
492 }
493 }
494 });
495 self.refresh_task = Some(handle);
496
497 Ok(())
498 }
499
500 fn handle_key_event(&mut self, event: KeyEvent, event_loop: &ActiveEventLoop) {
501 use winit::event::ElementState;
502 use winit::keyboard::{Key, NamedKey};
503
504 let any_ui_visible =
506 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
507
508 if any_ui_visible {
511 let is_ui_control_key = matches!(
512 event.logical_key,
513 Key::Named(NamedKey::F1)
514 | Key::Named(NamedKey::F2)
515 | Key::Named(NamedKey::F3)
516 | Key::Named(NamedKey::Escape)
517 );
518
519 if !is_ui_control_key {
520 log::debug!("Blocking key while UI visible: {:?}", event.logical_key);
521 return;
522 }
523 }
524
525 if self.is_egui_using_keyboard() {
527 log::debug!("Blocking key event: egui wants keyboard input");
528 return;
529 }
530
531 let is_running = if let Some(terminal) = &self.terminal {
533 if let Ok(term) = terminal.try_lock() {
534 term.is_running()
535 } else {
536 true
537 }
538 } else {
539 true
540 };
541
542 if !is_running && event.state == ElementState::Pressed {
545 log::info!("Shell has exited, closing terminal on keypress");
546 if let Some(task) = self.refresh_task.take() {
548 task.abort();
549 log::info!("Refresh task aborted");
550 }
551 event_loop.exit();
552 return;
553 }
554
555 if event.state == ElementState::Pressed {
557 self.last_key_press = Some(std::time::Instant::now());
558 }
559
560 if self.handle_scroll_keys(&event) {
562 return; }
564
565 if self.handle_config_reload(&event) {
567 return; }
569
570 if self.handle_clipboard_history_keys(&event) {
572 return; }
574
575 if self.handle_fullscreen_toggle(&event) {
577 return; }
579
580 if self.handle_help_toggle(&event) {
582 return; }
584
585 if self.handle_settings_toggle(&event) {
587 return; }
589
590 if self.handle_shader_editor_toggle(&event) {
592 return; }
594
595 if self.handle_fps_overlay_toggle(&event) {
597 return; }
599
600 if self.handle_utility_shortcuts(&event, event_loop) {
602 return; }
604
605 if event.state == ElementState::Pressed && self.selection.is_some() {
607 self.selection = None;
608 if let Some(window) = &self.window {
609 window.request_redraw();
610 }
611 }
612
613 let is_tab = matches!(event.logical_key, Key::Named(NamedKey::Tab));
615 let is_space = matches!(event.logical_key, Key::Named(NamedKey::Space));
616 if is_tab {
617 log::debug!("Tab key event received, state={:?}", event.state);
618 }
619 if is_space {
620 log::debug!("Space key event received, state={:?}", event.state);
621 }
622
623 if let Some(bytes) = self.input_handler.handle_key_event(event)
625 && let Some(terminal) = &self.terminal
626 {
627 if is_tab {
628 log::debug!("Sending Tab key to terminal ({} bytes)", bytes.len());
629 }
630 if is_space {
631 log::debug!("Sending Space key to terminal ({} bytes)", bytes.len());
632 }
633 let terminal_clone = Arc::clone(terminal);
634
635 self.runtime.spawn(async move {
636 let term = terminal_clone.lock().await;
637 let _ = term.write(&bytes);
638 });
639 }
640 }
641
642 fn handle_scroll_keys(&mut self, event: &KeyEvent) -> bool {
643 use winit::event::ElementState;
644 use winit::keyboard::{Key, NamedKey};
645
646 if event.state != ElementState::Pressed {
647 return false;
648 }
649
650 let shift = self.input_handler.modifiers.state().shift_key();
651
652 let handled = match &event.logical_key {
653 Key::Named(NamedKey::PageUp) => {
654 self.scroll_up_page();
656 true
657 }
658 Key::Named(NamedKey::PageDown) => {
659 self.scroll_down_page();
661 true
662 }
663 Key::Named(NamedKey::Home) if shift => {
664 self.scroll_to_top();
666 true
667 }
668 Key::Named(NamedKey::End) if shift => {
669 self.scroll_to_bottom();
671 true
672 }
673 _ => false,
674 };
675
676 if handled && let Some(window) = &self.window {
677 window.request_redraw();
678 }
679
680 handled
681 }
682
683 fn handle_config_reload(&mut self, event: &KeyEvent) -> bool {
684 use winit::event::ElementState;
685 use winit::keyboard::{Key, NamedKey};
686
687 if event.state != ElementState::Pressed {
688 return false;
689 }
690
691 if matches!(event.logical_key, Key::Named(NamedKey::F5)) {
693 log::info!("Reloading configuration (F5 pressed)");
694 self.reload_config();
695 return true;
696 }
697
698 false
699 }
700
701 fn reload_config(&mut self) {
702 match Config::load() {
703 Ok(new_config) => {
704 log::info!("Configuration reloaded successfully");
705
706 self.config.auto_copy_selection = new_config.auto_copy_selection;
710
711 self.config.middle_click_paste = new_config.middle_click_paste;
713
714 if self.config.window_title != new_config.window_title {
716 self.config.window_title = new_config.window_title.clone();
717 if let Some(window) = &self.window {
718 window.set_title(&new_config.window_title);
719 }
720 }
721
722 if self.config.theme != new_config.theme {
724 self.config.theme = new_config.theme.clone();
725 if let Some(terminal) = &self.terminal
726 && let Ok(mut term) = terminal.try_lock()
727 {
728 term.set_theme(new_config.load_theme());
729 log::info!("Applied new theme: {}", new_config.theme);
730 }
731 }
732
733 if new_config.font_size != self.config.font_size {
738 log::info!(
739 "Font size changed from {} -> {} (applied live)",
740 self.config.font_size,
741 new_config.font_size
742 );
743 }
744
745 if new_config.cols != self.config.cols || new_config.rows != self.config.rows {
746 log::warn!("Terminal dimensions change requires restart");
747 }
748
749 if let Some(window) = &self.window {
751 window.request_redraw();
752 }
753 }
754 Err(e) => {
755 log::error!("Failed to reload configuration: {}", e);
756 }
757 }
758 }
759
760 fn handle_clipboard_history_keys(&mut self, event: &KeyEvent) -> bool {
761 use winit::event::ElementState;
762 use winit::keyboard::Key;
763
764 if self.clipboard_history_ui.visible {
766 if event.state == ElementState::Pressed {
767 match &event.logical_key {
768 Key::Named(winit::keyboard::NamedKey::Escape) => {
769 self.clipboard_history_ui.visible = false;
770 self.needs_redraw = true;
771 return true;
772 }
773 Key::Named(winit::keyboard::NamedKey::ArrowUp) => {
774 self.clipboard_history_ui.select_previous();
775 self.needs_redraw = true;
776 return true;
777 }
778 Key::Named(winit::keyboard::NamedKey::ArrowDown) => {
779 self.clipboard_history_ui.select_next();
780 self.needs_redraw = true;
781 return true;
782 }
783 Key::Named(winit::keyboard::NamedKey::Enter) => {
784 if let Some(entry) = self.clipboard_history_ui.selected_entry() {
786 let content = entry.content.clone();
787 self.clipboard_history_ui.visible = false;
788 self.paste_text(&content);
789 self.needs_redraw = true;
790 }
791 return true;
792 }
793 _ => {}
794 }
795 }
796 return true;
798 }
799
800 if event.state == ElementState::Pressed {
802 let ctrl = self.input_handler.modifiers.state().control_key();
803 let shift = self.input_handler.modifiers.state().shift_key();
804
805 if ctrl
806 && shift
807 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "h" || c.as_str() == "H")
808 {
809 self.toggle_clipboard_history();
810 return true;
811 }
812 }
813
814 false
815 }
816
817 fn toggle_clipboard_history(&mut self) {
818 if let Some(terminal) = &self.terminal
820 && let Ok(term) = terminal.try_lock()
821 {
822 let mut all_entries = Vec::new();
824 all_entries.extend(term.get_clipboard_history(ClipboardSlot::Primary));
825 all_entries.extend(term.get_clipboard_history(ClipboardSlot::Clipboard));
826 all_entries.extend(term.get_clipboard_history(ClipboardSlot::Selection));
827
828 all_entries.sort_by_key(|e| std::cmp::Reverse(e.timestamp));
830
831 self.clipboard_history_ui.update_entries(all_entries);
832 }
833
834 self.clipboard_history_ui.toggle();
835 self.needs_redraw = true;
836 log::debug!(
837 "Clipboard history UI toggled: {}",
838 self.clipboard_history_ui.visible
839 );
840 }
841
842 fn paste_text(&mut self, text: &str) {
843 if let Some(terminal) = &self.terminal {
844 let terminal_clone = Arc::clone(terminal);
845 let text = text.replace('\n', "\r");
847 self.runtime.spawn(async move {
848 let term = terminal_clone.lock().await;
849 let _ = term.write(text.as_bytes());
850 log::debug!("Pasted text from clipboard history ({} bytes)", text.len());
851 });
852 }
853 }
854
855 fn force_surface_reconfigure(&mut self) {
859 log::info!("Force surface reconfigure triggered");
860
861 if let Some(renderer) = &mut self.renderer {
862 renderer.reconfigure_surface();
864
865 renderer.clear_glyph_cache();
867
868 self.cached_cells = None;
870 }
871
872 #[cfg(target_os = "macos")]
874 {
875 if let Some(window) = &self.window
876 && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
877 {
878 log::warn!("Failed to reconfigure Metal layer: {}", e);
879 }
880 }
881
882 if let Some(window) = &self.window {
884 window.request_redraw();
885 }
886
887 self.needs_redraw = true;
888 }
889
890 fn handle_utility_shortcuts(
891 &mut self,
892 event: &KeyEvent,
893 _event_loop: &ActiveEventLoop,
894 ) -> bool {
895 use winit::event::ElementState;
896 use winit::keyboard::Key;
897
898 if event.state != ElementState::Pressed {
899 return false;
900 }
901
902 let ctrl = self.input_handler.modifiers.state().control_key();
903 let shift = self.input_handler.modifiers.state().shift_key();
904
905 if ctrl
907 && shift
908 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "k" || c.as_str() == "K")
909 {
910 let cleared = if let Some(terminal) = &self.terminal
912 && let Ok(term) = terminal.try_lock()
913 {
914 term.clear_scrollback();
915 true
916 } else {
917 false
918 };
919
920 if cleared {
921 self.cached_scrollback_len = 0;
922 self.set_scroll_target(0);
923 log::info!("Cleared scrollback buffer");
924 }
925 return true;
926 }
927
928 if ctrl
930 && !shift
931 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "l" || c.as_str() == "L")
932 {
933 if let Some(terminal) = &self.terminal {
934 let terminal_clone = Arc::clone(terminal);
935 let clear_sequence = vec![0x0C]; self.runtime.spawn(async move {
938 if let Ok(term) = terminal_clone.try_lock() {
939 let _ = term.write(&clear_sequence);
940 log::debug!("Sent clear screen sequence (Ctrl+L)");
941 }
942 });
943 }
944 return true;
945 }
946
947 if ctrl
949 && !shift
950 && (matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "+" || c.as_str() == "="))
951 {
952 self.config.font_size = (self.config.font_size + 1.0).min(72.0);
953 self.pending_font_rebuild = true;
954 log::info!(
955 "Font size increased to {} (applying live)",
956 self.config.font_size
957 );
958 if let Some(window) = &self.window {
959 window.request_redraw();
960 }
961 return true;
962 }
963
964 if ctrl
966 && !shift
967 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "-" || c.as_str() == "_")
968 {
969 self.config.font_size = (self.config.font_size - 1.0).max(6.0);
970 self.pending_font_rebuild = true;
971 log::info!(
972 "Font size decreased to {} (applying live)",
973 self.config.font_size
974 );
975 if let Some(window) = &self.window {
976 window.request_redraw();
977 }
978 return true;
979 }
980
981 if ctrl && !shift && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "0")
983 {
984 self.config.font_size = 14.0; self.pending_font_rebuild = true;
986 log::info!("Font size reset to default (14.0, applying live)");
987 if let Some(window) = &self.window {
988 window.request_redraw();
989 }
990 return true;
991 }
992
993 if ctrl
995 && shift
996 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "s" || c.as_str() == "S")
997 {
998 self.take_screenshot();
999 return true;
1000 }
1001
1002 if ctrl
1004 && shift
1005 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "r" || c.as_str() == "R")
1006 {
1007 self.toggle_recording();
1008 return true;
1009 }
1010
1011 if ctrl && shift && matches!(event.logical_key, Key::Named(winit::keyboard::NamedKey::F5)) {
1013 log::info!("Manual surface reconfigure triggered via Ctrl+Shift+F5");
1014 self.force_surface_reconfigure();
1015 return true;
1016 }
1017
1018 false
1019 }
1020
1021 fn handle_fullscreen_toggle(&mut self, event: &KeyEvent) -> bool {
1022 use winit::event::ElementState;
1023 use winit::keyboard::{Key, NamedKey};
1024
1025 if event.state != ElementState::Pressed {
1026 return false;
1027 }
1028
1029 if matches!(event.logical_key, Key::Named(NamedKey::F11))
1031 && let Some(window) = &self.window
1032 {
1033 self.is_fullscreen = !self.is_fullscreen;
1034
1035 if self.is_fullscreen {
1036 window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
1037 log::info!("Entering fullscreen mode");
1038 } else {
1039 window.set_fullscreen(None);
1040 log::info!("Exiting fullscreen mode");
1041 }
1042
1043 return true;
1044 }
1045
1046 false
1047 }
1048
1049 fn handle_settings_toggle(&mut self, event: &KeyEvent) -> bool {
1050 use winit::event::ElementState;
1051 use winit::keyboard::{Key, NamedKey};
1052
1053 if event.state != ElementState::Pressed {
1054 return false;
1055 }
1056
1057 if matches!(event.logical_key, Key::Named(NamedKey::F12)) {
1059 self.settings_ui.toggle();
1060 log::info!(
1061 "Settings UI toggled: {}",
1062 if self.settings_ui.visible {
1063 "visible"
1064 } else {
1065 "hidden"
1066 }
1067 );
1068
1069 if let Some(window) = &self.window {
1071 window.request_redraw();
1072 }
1073
1074 return true;
1075 }
1076
1077 false
1078 }
1079
1080 fn handle_help_toggle(&mut self, event: &KeyEvent) -> bool {
1082 use winit::event::ElementState;
1083 use winit::keyboard::{Key, NamedKey};
1084
1085 if event.state != ElementState::Pressed {
1086 return false;
1087 }
1088
1089 if matches!(event.logical_key, Key::Named(NamedKey::F1)) {
1091 self.help_ui.toggle();
1092 log::info!(
1093 "Help UI toggled: {}",
1094 if self.help_ui.visible {
1095 "visible"
1096 } else {
1097 "hidden"
1098 }
1099 );
1100
1101 if let Some(window) = &self.window {
1103 window.request_redraw();
1104 }
1105
1106 return true;
1107 }
1108
1109 if matches!(event.logical_key, Key::Named(NamedKey::Escape)) && self.help_ui.visible {
1111 self.help_ui.visible = false;
1112 log::info!("Help UI closed via Escape");
1113
1114 if let Some(window) = &self.window {
1115 window.request_redraw();
1116 }
1117
1118 return true;
1119 }
1120
1121 false
1122 }
1123
1124 fn handle_shader_editor_toggle(&mut self, event: &KeyEvent) -> bool {
1126 use winit::event::ElementState;
1127 use winit::keyboard::{Key, NamedKey};
1128
1129 if event.state != ElementState::Pressed {
1130 return false;
1131 }
1132
1133 if matches!(event.logical_key, Key::Named(NamedKey::F11)) {
1135 if self.settings_ui.is_shader_editor_visible() {
1136 log::info!("Shader editor close requested via F11");
1138 } else {
1139 if self.settings_ui.open_shader_editor() {
1141 log::info!("Shader editor opened via F11");
1142 } else {
1143 log::warn!("Cannot open shader editor: no shader path configured in settings");
1144 }
1145 }
1146
1147 if let Some(window) = &self.window {
1149 window.request_redraw();
1150 }
1151
1152 return true;
1153 }
1154
1155 false
1156 }
1157
1158 fn handle_fps_overlay_toggle(&mut self, event: &KeyEvent) -> bool {
1160 use winit::event::ElementState;
1161 use winit::keyboard::{Key, NamedKey};
1162
1163 if event.state != ElementState::Pressed {
1164 return false;
1165 }
1166
1167 if matches!(event.logical_key, Key::Named(NamedKey::F3)) {
1169 self.show_fps_overlay = !self.show_fps_overlay;
1170 log::info!(
1171 "FPS overlay toggled: {}",
1172 if self.show_fps_overlay {
1173 "visible"
1174 } else {
1175 "hidden"
1176 }
1177 );
1178
1179 if let Some(window) = &self.window {
1181 window.request_redraw();
1182 }
1183
1184 return true;
1185 }
1186
1187 false
1188 }
1189
1190 fn scroll_up_page(&mut self) {
1191 if let Some(renderer) = &self.renderer {
1193 let char_height = self.config.font_size * 1.2;
1194 let page_size = (renderer.size().height as f32 / char_height) as usize;
1195
1196 let new_target = self.scroll_state.target_offset.saturating_add(page_size);
1197 let clamped_target = new_target.min(self.cached_scrollback_len);
1198 self.set_scroll_target(clamped_target);
1199 }
1200 }
1201
1202 fn scroll_down_page(&mut self) {
1203 if let Some(renderer) = &self.renderer {
1205 let char_height = self.config.font_size * 1.2;
1206 let page_size = (renderer.size().height as f32 / char_height) as usize;
1207
1208 let new_target = self.scroll_state.target_offset.saturating_sub(page_size);
1209 self.set_scroll_target(new_target);
1210 }
1211 }
1212
1213 fn scroll_to_top(&mut self) {
1214 self.set_scroll_target(self.cached_scrollback_len);
1215 }
1216
1217 fn scroll_to_bottom(&mut self) {
1218 self.set_scroll_target(0);
1219 }
1220
1221 fn is_egui_using_pointer(&self) -> bool {
1223 let any_ui_visible =
1225 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
1226 if !any_ui_visible {
1227 return false;
1228 }
1229
1230 if let Some(ctx) = &self.egui_ctx {
1232 ctx.is_using_pointer() || ctx.wants_pointer_input()
1233 } else {
1234 false
1235 }
1236 }
1237
1238 fn is_egui_using_keyboard(&self) -> bool {
1240 let any_ui_visible =
1242 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
1243 if !any_ui_visible {
1244 return false;
1245 }
1246
1247 if let Some(ctx) = &self.egui_ctx {
1249 ctx.wants_keyboard_input()
1250 } else {
1251 false
1252 }
1253 }
1254
1255 fn handle_mouse_wheel(&mut self, delta: MouseScrollDelta) {
1256 if let Some(terminal) = &self.terminal
1260 && let Ok(term) = terminal.try_lock()
1261 && term.is_mouse_tracking_enabled()
1262 {
1263 let scroll_lines = match delta {
1265 MouseScrollDelta::LineDelta(_x, y) => y as i32,
1266 MouseScrollDelta::PixelDelta(pos) => (pos.y / 20.0) as i32,
1267 };
1268
1269 if let Some((col, row)) =
1271 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1272 {
1273 let button = if scroll_lines > 0 { 64 } else { 65 };
1275 let count = scroll_lines.unsigned_abs().min(10);
1277
1278 let mut all_encoded = Vec::new();
1280 for _ in 0..count {
1281 let encoded = term.encode_mouse_event(button, col, row, true, 0);
1282 if !encoded.is_empty() {
1283 all_encoded.extend(encoded);
1284 }
1285 }
1286
1287 if !all_encoded.is_empty() {
1288 let terminal_clone = Arc::clone(terminal);
1289 let runtime = Arc::clone(&self.runtime);
1290 runtime.spawn(async move {
1291 let t = terminal_clone.lock().await;
1292 let _ = t.write(&all_encoded);
1293 });
1294 }
1295 }
1296 return; }
1298
1299 let scroll_lines = match delta {
1302 MouseScrollDelta::LineDelta(_x, y) => (y * self.config.mouse_scroll_speed) as i32,
1303 MouseScrollDelta::PixelDelta(pos) => (pos.y / 20.0) as i32,
1304 };
1305
1306 let scrollback_len = self.cached_scrollback_len;
1307
1308 let new_target = self.scroll_state.apply_scroll(scroll_lines, scrollback_len);
1310
1311 self.set_scroll_target(new_target);
1313 }
1314
1315 fn set_scroll_target(&mut self, new_offset: usize) {
1318 if self.scroll_state.set_target(new_offset) {
1319 if let Some(window) = &self.window {
1322 window.request_redraw();
1323 }
1324 }
1325 }
1326
1327 fn update_scroll_animation(&mut self) -> bool {
1330 self.scroll_state.update_animation()
1331 }
1332
1333 fn pixel_to_cell(&self, x: f64, y: f64) -> Option<(usize, usize)> {
1335 if let Some(renderer) = &self.renderer {
1336 let cell_width = renderer.cell_width() as f64;
1338 let cell_height = renderer.cell_height() as f64;
1339 let padding = renderer.window_padding() as f64;
1340
1341 let adjusted_x = (x - padding).max(0.0);
1343 let adjusted_y = (y - padding).max(0.0);
1344
1345 let col = (adjusted_x / cell_width) as usize;
1346 let row = (adjusted_y / cell_height) as usize;
1347
1348 Some((col, row))
1349 } else {
1350 None
1351 }
1352 }
1353
1354 fn should_show_scrollbar(&self) -> bool {
1356 if self.cached_scrollback_len == 0 {
1358 return false;
1359 }
1360
1361 if self.scroll_state.dragging {
1363 return true;
1364 }
1365
1366 if self.config.scrollbar_autohide_delay == 0 {
1368 return true;
1369 }
1370
1371 if self.scroll_state.offset > 0 || self.scroll_state.target_offset > 0 {
1373 return true;
1374 }
1375
1376 if let Some(window) = &self.window {
1378 let padding = 32.0; let width = window.inner_size().width as f64;
1380 let near_right = self.config.scrollbar_position != "left"
1381 && (width - self.mouse_position.0) <= padding;
1382 let near_left =
1383 self.config.scrollbar_position == "left" && self.mouse_position.0 <= padding;
1384 if near_left || near_right {
1385 return true;
1386 }
1387 }
1388
1389 self.scroll_state.last_activity.elapsed().as_millis()
1391 < self.config.scrollbar_autohide_delay as u128
1392 }
1393
1394 fn select_word_at(&mut self, col: usize, row: usize) {
1396 if let Some(terminal) = &self.terminal
1397 && let Ok(term) = terminal.try_lock()
1398 {
1399 let (cols, _rows) = term.dimensions();
1400 let visible_cells =
1401 term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1402 if visible_cells.is_empty() || cols == 0 {
1403 return;
1404 }
1405
1406 let cell_idx = row * cols + col;
1407 if cell_idx >= visible_cells.len() {
1408 return;
1409 }
1410
1411 let mut start_col = col;
1413 let mut end_col = col;
1414
1415 for c in (0..col).rev() {
1417 let idx = row * cols + c;
1418 if idx >= visible_cells.len() {
1419 break;
1420 }
1421 let ch = visible_cells[idx].grapheme.chars().next().unwrap_or('\0');
1422 if ch.is_alphanumeric() || ch == '_' {
1423 start_col = c;
1424 } else {
1425 break;
1426 }
1427 }
1428
1429 for c in col..cols {
1431 let idx = row * cols + c;
1432 if idx >= visible_cells.len() {
1433 break;
1434 }
1435 let ch = visible_cells[idx].grapheme.chars().next().unwrap_or('\0');
1436 if ch.is_alphanumeric() || ch == '_' {
1437 end_col = c;
1438 } else {
1439 break;
1440 }
1441 }
1442
1443 self.selection = Some(Selection::new(
1444 (start_col, row),
1445 (end_col, row),
1446 SelectionMode::Normal,
1447 ));
1448 }
1449 }
1450
1451 fn select_line_at(&mut self, row: usize) {
1453 if let Some(terminal) = &self.terminal
1454 && let Ok(term) = terminal.try_lock()
1455 {
1456 let (cols, _rows) = term.dimensions();
1457 if cols == 0 {
1458 return;
1459 }
1460
1461 self.selection = Some(Selection::new(
1463 (0, row),
1464 (cols.saturating_sub(1), row),
1465 SelectionMode::Line,
1466 ));
1467 }
1468 }
1469
1470 fn extend_line_selection(&mut self, current_row: usize) {
1472 if let Some(terminal) = &self.terminal
1473 && let Ok(term) = terminal.try_lock()
1474 {
1475 let (cols, _rows) = term.dimensions();
1476 if cols == 0 {
1477 return;
1478 }
1479
1480 let anchor_row = self.click_position.map(|(_, r)| r).unwrap_or(current_row);
1482
1483 if let Some(ref mut selection) = self.selection
1484 && selection.mode == SelectionMode::Line
1485 {
1486 if current_row >= anchor_row {
1489 selection.start = (0, anchor_row);
1491 selection.end = (cols.saturating_sub(1), current_row);
1492 } else {
1493 selection.start = (cols.saturating_sub(1), anchor_row);
1496 selection.end = (0, current_row);
1497 }
1498 }
1499 }
1500 }
1501
1502 fn get_selected_text(&self) -> Option<String> {
1504 if let (Some(selection), Some(terminal)) = (&self.selection, &self.terminal) {
1505 if let Ok(term) = terminal.try_lock() {
1506 let (start, end) = selection.normalized();
1507 let (start_col, start_row) = start;
1508 let (end_col, end_row) = end;
1509
1510 let (cols, rows) = term.dimensions();
1511 let visible_cells =
1512 term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1513 if visible_cells.is_empty() || cols == 0 {
1514 return None;
1515 }
1516
1517 let mut visible_lines = Vec::with_capacity(rows);
1518 for row in 0..rows {
1519 let start_idx = row * cols;
1520 let end_idx = start_idx.saturating_add(cols);
1521 if end_idx > visible_cells.len() {
1522 break;
1523 }
1524
1525 let mut line = String::with_capacity(cols);
1526 for cell in &visible_cells[start_idx..end_idx] {
1527 line.push_str(&cell.grapheme);
1528 }
1529 visible_lines.push(line);
1530 }
1531
1532 if visible_lines.is_empty() {
1533 return None;
1534 }
1535
1536 let mut selected_text = String::new();
1537 let max_row = visible_lines.len().saturating_sub(1);
1538 let start_row = start_row.min(max_row);
1539 let end_row = end_row.min(max_row);
1540
1541 if selection.mode == SelectionMode::Line {
1542 #[allow(clippy::needless_range_loop)]
1544 for row in start_row..=end_row {
1545 if row > start_row {
1546 selected_text.push('\n');
1547 }
1548 let line = &visible_lines[row];
1549 selected_text.push_str(line.trim_end());
1551 }
1552 } else if selection.mode == SelectionMode::Rectangular {
1553 let min_col = start_col.min(end_col);
1555 let max_col = start_col.max(end_col);
1556
1557 #[allow(clippy::needless_range_loop)]
1558 for row in start_row..=end_row {
1559 if row > start_row {
1560 selected_text.push('\n');
1561 }
1562 let line = &visible_lines[row];
1563 selected_text.push_str(&Self::extract_columns(
1564 line,
1565 min_col,
1566 Some(max_col),
1567 ));
1568 }
1569 } else if start_row == end_row {
1570 let line = &visible_lines[start_row];
1572 selected_text = Self::extract_columns(line, start_col, Some(end_col));
1573 } else {
1574 for (idx, row) in (start_row..=end_row).enumerate() {
1576 let line = &visible_lines[row];
1577 if idx == 0 {
1578 selected_text.push_str(&Self::extract_columns(line, start_col, None));
1579 } else if row == end_row {
1580 selected_text.push('\n');
1581 selected_text.push_str(&Self::extract_columns(line, 0, Some(end_col)));
1582 } else {
1583 selected_text.push('\n');
1584 selected_text.push_str(line);
1585 }
1586 }
1587 }
1588
1589 Some(selected_text)
1590 } else {
1591 None
1592 }
1593 } else {
1594 None
1595 }
1596 }
1597
1598 fn detect_urls(&mut self) {
1600 self.detected_urls.clear();
1601
1602 if let Some(terminal) = &self.terminal
1603 && let Ok(term) = terminal.try_lock()
1604 {
1605 let (cols, rows) = term.dimensions();
1606 let visible_cells =
1607 term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1608
1609 if visible_cells.is_empty() || cols == 0 {
1610 return;
1611 }
1612
1613 let mut hyperlink_urls = std::collections::HashMap::new();
1615 let all_hyperlinks = term.get_all_hyperlinks();
1616 for hyperlink_info in all_hyperlinks {
1617 if let Some((col, row)) = hyperlink_info.positions.first() {
1619 let cell_idx = row * cols + col;
1621 if let Some(cell) = visible_cells.get(cell_idx)
1622 && let Some(id) = cell.hyperlink_id
1623 {
1624 hyperlink_urls.insert(id, hyperlink_info.url.clone());
1625 }
1626 }
1627 }
1628
1629 for row in 0..rows {
1631 let start_idx = row * cols;
1632 let end_idx = start_idx.saturating_add(cols);
1633 if end_idx > visible_cells.len() {
1634 break;
1635 }
1636
1637 let row_cells = &visible_cells[start_idx..end_idx];
1638
1639 let mut line = String::with_capacity(cols);
1640 for cell in row_cells {
1641 line.push_str(&cell.grapheme);
1642 }
1643
1644 let absolute_row = row + self.scroll_state.offset;
1646
1647 let regex_urls = url_detection::detect_urls_in_line(&line, absolute_row);
1649 self.detected_urls.extend(regex_urls);
1650
1651 let osc8_urls =
1653 url_detection::detect_osc8_hyperlinks(row_cells, absolute_row, &hyperlink_urls);
1654 self.detected_urls.extend(osc8_urls);
1655 }
1656 }
1657 }
1658
1659 fn apply_url_underlines(
1662 &self,
1663 cells: &mut [crate::cell_renderer::Cell],
1664 renderer_size: &winit::dpi::PhysicalSize<u32>,
1665 ) {
1666 if self.detected_urls.is_empty() {
1667 return;
1668 }
1669
1670 let char_width = self.config.font_size * 0.6;
1672 let cols = (renderer_size.width as f32 / char_width) as usize;
1673
1674 let url_color = [79, 195, 247, 255];
1676
1677 for url in &self.detected_urls {
1679 if url.row < self.scroll_state.offset {
1681 continue; }
1683 let viewport_row = url.row - self.scroll_state.offset;
1684
1685 for col in url.start_col..url.end_col {
1687 let cell_idx = viewport_row * cols + col;
1688 if cell_idx < cells.len() {
1689 cells[cell_idx].fg_color = url_color;
1690 cells[cell_idx].underline = true; }
1692 }
1693 }
1694 }
1695
1696 fn try_send_mouse_event(&self, button: u8, pressed: bool) -> bool {
1701 if let Some(terminal) = &self.terminal
1702 && let Some((col, row)) =
1703 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1704 && let Ok(term) = terminal.try_lock()
1705 {
1706 let alt_screen_active = term.is_alt_screen_active();
1709
1710 if term.is_mouse_tracking_enabled() {
1712 let encoded = term.encode_mouse_event(button, col, row, pressed, 0);
1714
1715 if !encoded.is_empty() {
1716 let terminal_clone = Arc::clone(terminal);
1718 let runtime = Arc::clone(&self.runtime);
1719 runtime.spawn(async move {
1720 let t = terminal_clone.lock().await;
1721 let _ = t.write(&encoded);
1722 });
1723 }
1724 return true; }
1726
1727 if alt_screen_active {
1729 return true;
1730 }
1731 }
1732 false }
1734
1735 fn update_cursor_blink(&mut self) {
1737 if !self.config.cursor_blink {
1738 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1740 return;
1741 }
1742
1743 let now = std::time::Instant::now();
1744
1745 if let Some(last_key) = self.last_key_press
1747 && now.duration_since(last_key).as_millis() < 500
1748 {
1749 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1750 self.last_cursor_blink = Some(now);
1751 return;
1752 }
1753
1754 let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
1756
1757 if let Some(last_blink) = self.last_cursor_blink {
1758 let elapsed = now.duration_since(last_blink);
1759 let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
1760
1761 self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
1763 .abs()
1764 .clamp(0.0, 1.0);
1765
1766 if elapsed >= blink_interval * 2 {
1768 self.last_cursor_blink = Some(now);
1769 }
1770 } else {
1771 self.cursor_opacity = 1.0;
1773 self.last_cursor_blink = Some(now);
1774 }
1775 }
1776
1777 fn handle_mouse_button(&mut self, button: MouseButton, state: ElementState) {
1778 self.mouse_button_pressed = state == ElementState::Pressed;
1780
1781 if button == MouseButton::Left
1784 && let Some(ref mut renderer) = self.renderer
1785 {
1786 renderer.set_shader_mouse_button(
1787 state == ElementState::Pressed,
1788 self.mouse_position.0 as f32,
1789 self.mouse_position.1 as f32,
1790 );
1791 }
1792
1793 match button {
1794 MouseButton::Left => {
1795 if state == ElementState::Pressed
1798 && self.input_handler.modifiers.state().control_key()
1799 && let Some((col, row)) =
1800 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1801 {
1802 let adjusted_row = row + self.scroll_state.offset;
1804
1805 if let Some(url) =
1806 url_detection::find_url_at_position(&self.detected_urls, col, adjusted_row)
1807 {
1808 if let Err(e) = url_detection::open_url(&url.url) {
1809 log::error!("Failed to open URL: {}", e);
1810 }
1811 return; }
1813 }
1814
1815 if self.try_send_mouse_event(0, state == ElementState::Pressed) {
1818 return; }
1820
1821 if state == ElementState::Pressed {
1822 let mouse_x = self.mouse_position.0 as f32;
1825 let mouse_y = self.mouse_position.1 as f32;
1826
1827 if let Some(renderer) = &self.renderer
1828 && renderer.scrollbar_track_contains_x(mouse_x)
1829 {
1830 self.scroll_state.dragging = true;
1831 self.scroll_state.last_activity = std::time::Instant::now();
1832
1833 let thumb_bounds = renderer.scrollbar_thumb_bounds();
1834 if renderer.scrollbar_contains_point(mouse_x, mouse_y) {
1835 self.scroll_state.drag_offset = thumb_bounds
1837 .map(|(thumb_top, thumb_height)| {
1838 (mouse_y - thumb_top).clamp(0.0, thumb_height)
1839 })
1840 .unwrap_or(0.0);
1841 } else {
1842 self.scroll_state.drag_offset = thumb_bounds
1844 .map(|(_, thumb_height)| thumb_height / 2.0)
1845 .unwrap_or(0.0);
1846 }
1847
1848 self.drag_scrollbar_to(mouse_y);
1849 return; }
1851
1852 if let Some((col, row)) =
1855 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1856 {
1857 let now = std::time::Instant::now();
1858 let same_position = self.click_position == Some((col, row));
1859
1860 let threshold_ms = if self.click_count == 1 {
1862 self.config.mouse_double_click_threshold
1863 } else {
1864 self.config.mouse_triple_click_threshold
1865 };
1866 let click_threshold = std::time::Duration::from_millis(threshold_ms);
1867
1868 if same_position
1870 && let Some(last_time) = self.last_click_time
1871 && now.duration_since(last_time) < click_threshold
1872 {
1873 self.click_count = (self.click_count + 1).min(3);
1874 } else {
1875 self.click_count = 1;
1876 self.selection = None;
1878 }
1879
1880 self.last_click_time = Some(now);
1881 self.click_position = Some((col, row));
1882
1883 if self.click_count == 2 {
1885 self.select_word_at(col, row);
1887 self.is_selecting = false; if let Some(window) = &self.window {
1889 window.request_redraw();
1890 }
1891 } else if self.click_count == 3 {
1892 self.select_line_at(row);
1894 self.is_selecting = true; if let Some(window) = &self.window {
1896 window.request_redraw();
1897 }
1898 } else {
1899 self.is_selecting = false;
1901 self.selection = None;
1902 if let Some(window) = &self.window {
1903 window.request_redraw();
1904 }
1905 }
1906 }
1907 } else {
1908 if self.scroll_state.dragging {
1910 self.scroll_state.dragging = false;
1911 self.scroll_state.drag_offset = 0.0;
1912 return;
1913 }
1914
1915 self.is_selecting = false;
1917
1918 if let Some(mut selected_text) = self.get_selected_text()
1919 && !selected_text.is_empty()
1920 {
1921 if !self.config.copy_trailing_newline {
1923 while selected_text.ends_with('\n') || selected_text.ends_with('\r') {
1924 selected_text.pop();
1925 }
1926 }
1927
1928 if let Err(e) = self.input_handler.copy_to_primary_selection(&selected_text)
1930 {
1931 log::debug!("Failed to copy to primary selection: {}", e);
1932 } else {
1933 log::debug!(
1934 "Copied {} chars to primary selection",
1935 selected_text.len()
1936 );
1937 }
1938
1939 if self.config.auto_copy_selection {
1941 if let Err(e) = self.input_handler.copy_to_clipboard(&selected_text) {
1942 log::error!("Failed to copy to clipboard: {}", e);
1943 } else {
1944 log::debug!("Copied {} chars to clipboard", selected_text.len());
1945 }
1946 }
1947
1948 if let Some(terminal) = &self.terminal
1950 && let Ok(term) = terminal.try_lock()
1951 {
1952 term.add_to_clipboard_history(
1953 ClipboardSlot::Clipboard,
1954 selected_text.clone(),
1955 None,
1956 );
1957 }
1958 }
1959 }
1960 }
1961 MouseButton::Middle => {
1962 if self.try_send_mouse_event(1, state == ElementState::Pressed) {
1964 return; }
1966
1967 if state == ElementState::Pressed && self.config.middle_click_paste {
1969 if let Some(bytes) = self.input_handler.paste_from_primary_selection()
1971 && let Some(terminal) = &self.terminal
1972 {
1973 let terminal_clone = Arc::clone(terminal);
1974 self.runtime.spawn(async move {
1975 let term = terminal_clone.lock().await;
1976 let _ = term.write(&bytes);
1977 });
1978 }
1979 }
1980 }
1981 MouseButton::Right => {
1982 let _ = self.try_send_mouse_event(2, state == ElementState::Pressed);
1984 }
1986 _ => {}
1987 }
1988 }
1989
1990 fn handle_mouse_move(&mut self, position: (f64, f64)) {
1991 self.mouse_position = position;
1992
1993 if let Some(ref mut renderer) = self.renderer {
1996 renderer.set_shader_mouse_position(position.0 as f32, position.1 as f32);
1997 }
1998
1999 if let Some((col, row)) = self.pixel_to_cell(position.0, position.1) {
2002 let adjusted_row = row + self.scroll_state.offset;
2003 let url_opt =
2004 url_detection::find_url_at_position(&self.detected_urls, col, adjusted_row);
2005
2006 if let Some(url) = url_opt {
2007 if self.hovered_url.as_ref() != Some(&url.url) {
2009 self.hovered_url = Some(url.url.clone());
2010 if let Some(window) = &self.window {
2011 window.set_cursor(winit::window::CursorIcon::Pointer);
2013 let tooltip_title = format!("{} - {}", self.config.window_title, url.url);
2014 window.set_title(&tooltip_title);
2015 }
2016 }
2017 } else {
2018 if self.hovered_url.is_some() {
2020 self.hovered_url = None;
2021 if let Some(window) = &self.window {
2022 window.set_cursor(winit::window::CursorIcon::Text);
2023 if self.config.allow_title_change && !self.cached_terminal_title.is_empty()
2025 {
2026 window.set_title(&self.cached_terminal_title);
2027 } else {
2028 window.set_title(&self.config.window_title);
2029 }
2030 }
2031 }
2032 }
2033 }
2034
2035 if let Some(terminal) = &self.terminal
2038 && let Some((col, row)) = self.pixel_to_cell(position.0, position.1)
2039 && let Ok(term) = terminal.try_lock()
2040 && term.should_report_mouse_motion(self.mouse_button_pressed)
2041 {
2042 let button = if self.mouse_button_pressed {
2044 32 } else {
2046 35 };
2048
2049 let encoded = term.encode_mouse_event(button, col, row, true, 0);
2050 if !encoded.is_empty() {
2051 let terminal_clone = Arc::clone(terminal);
2052 let runtime = Arc::clone(&self.runtime);
2053 runtime.spawn(async move {
2054 let t = terminal_clone.lock().await;
2055 let _ = t.write(&encoded);
2056 });
2057 }
2058 return; }
2060
2061 if self.scroll_state.dragging {
2063 self.scroll_state.last_activity = std::time::Instant::now();
2064 self.drag_scrollbar_to(position.1 as f32);
2065 return; }
2067
2068 let alt_screen_active = self
2071 .terminal
2072 .as_ref()
2073 .and_then(|t| t.try_lock().ok())
2074 .is_some_and(|term| term.is_alt_screen_active());
2075
2076 if let Some((col, row)) = self.pixel_to_cell(position.0, position.1)
2077 && self.mouse_button_pressed
2078 && !alt_screen_active
2079 {
2080 if self.click_count == 1 && !self.is_selecting {
2081 if let Some(click_pos) = self.click_position
2083 && click_pos != (col, row)
2084 {
2085 self.is_selecting = true;
2086 let mode = if self.input_handler.modifiers.state().alt_key() {
2088 SelectionMode::Rectangular
2089 } else {
2090 SelectionMode::Normal
2091 };
2092 self.selection = Some(Selection::new(
2093 self.click_position.unwrap(),
2094 (col, row),
2095 mode,
2096 ));
2097 if let Some(window) = &self.window {
2098 window.request_redraw();
2099 }
2100 }
2101 } else if self.is_selecting {
2102 if let Some(ref selection) = self.selection {
2104 if selection.mode == SelectionMode::Line {
2105 self.extend_line_selection(row);
2107 if let Some(window) = &self.window {
2108 window.request_redraw();
2109 }
2110 } else {
2111 if let Some(ref mut sel) = self.selection {
2113 sel.end = (col, row);
2114 if let Some(window) = &self.window {
2115 window.request_redraw();
2116 }
2117 }
2118 }
2119 }
2120 }
2121 }
2122 }
2123
2124 fn drag_scrollbar_to(&mut self, mouse_y: f32) {
2125 if let Some(renderer) = &self.renderer {
2126 let adjusted_y = mouse_y - self.scroll_state.drag_offset;
2127 if let Some(new_offset) = renderer.scrollbar_mouse_y_to_scroll_offset(adjusted_y)
2128 && self.scroll_state.offset != new_offset
2129 {
2130 self.scroll_state.offset = new_offset;
2132 self.scroll_state.target_offset = new_offset;
2133 self.scroll_state.animated_offset = new_offset as f64;
2134 self.scroll_state.animation_start = None;
2135
2136 if let Some(window) = &self.window {
2137 window.request_redraw();
2138 }
2139 }
2140 }
2141 }
2142
2143 fn render(&mut self) {
2144 if self.is_shutting_down {
2146 return;
2147 }
2148
2149 let absolute_start = std::time::Instant::now();
2150
2151 self.needs_redraw = false;
2154
2155 let frame_start = std::time::Instant::now();
2157
2158 if let Some(last_start) = self.debug_last_frame_start {
2160 let frame_time = frame_start.duration_since(last_start);
2161 self.debug_frame_times.push(frame_time);
2162 if self.debug_frame_times.len() > 60 {
2163 self.debug_frame_times.remove(0);
2164 }
2165 }
2166 self.debug_last_frame_start = Some(frame_start);
2167
2168 let animation_running = self.update_scroll_animation();
2170
2171 if self.pending_font_rebuild {
2173 if let Err(e) = self.rebuild_renderer() {
2174 log::error!("Failed to rebuild renderer after font change: {}", e);
2175 }
2176 self.pending_font_rebuild = false;
2177 }
2178
2179 let (renderer_size, visible_lines) = if let Some(renderer) = &self.renderer {
2180 (renderer.size(), renderer.grid_size().1)
2181 } else {
2182 return;
2183 };
2184
2185 let terminal = if let Some(terminal) = &self.terminal {
2186 terminal
2187 } else {
2188 return;
2189 };
2190
2191 let _is_running = if let Ok(term) = terminal.try_lock() {
2193 term.is_running()
2194 } else {
2195 true };
2197
2198 if animation_running && let Some(window) = &self.window {
2200 window.request_redraw();
2201 }
2202
2203 let (cells, current_cursor_pos, cursor_style) = if let Ok(term) = terminal.try_lock() {
2205 let current_generation = term.update_generation();
2207
2208 let (selection, rectangular) = if let Some(sel) = self.selection {
2210 (
2211 Some(sel.normalized()),
2212 sel.mode == SelectionMode::Rectangular,
2213 )
2214 } else {
2215 (None, false)
2216 };
2217
2218 let current_cursor_pos = if self.scroll_state.offset == 0 && term.is_cursor_visible() {
2221 Some(term.cursor_position())
2222 } else {
2223 None
2224 };
2225
2226 let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
2227
2228 let cursor_style = if current_cursor_pos.is_some() {
2230 Some(term.cursor_style())
2231 } else {
2232 None
2233 };
2234
2235 log::trace!(
2236 "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
2237 current_cursor_pos,
2238 self.cursor_opacity,
2239 cursor_style,
2240 self.scroll_state.offset,
2241 term.is_cursor_visible()
2242 );
2243
2244 let needs_regeneration = self.cached_cells.is_none()
2247 || current_generation != self.last_generation
2248 || self.scroll_state.offset != self.last_scroll_offset
2249 || current_cursor_pos != self.last_cursor_pos || self.selection != self.last_selection; let cell_gen_start = std::time::Instant::now();
2253 let (cells, is_cache_hit) = if needs_regeneration {
2254 let fresh_cells = term.get_cells_with_scrollback(
2256 self.scroll_state.offset,
2257 selection,
2258 rectangular,
2259 cursor,
2260 );
2261
2262 self.cached_cells = Some(fresh_cells.clone());
2264 self.last_generation = current_generation;
2265 self.last_scroll_offset = self.scroll_state.offset;
2266 self.last_cursor_pos = current_cursor_pos;
2267 self.last_selection = self.selection;
2268
2269 (fresh_cells, false)
2270 } else {
2271 (self.cached_cells.as_ref().unwrap().clone(), true)
2274 };
2275 self.debug_cache_hit = is_cache_hit;
2276 self.debug_cell_gen_time = cell_gen_start.elapsed();
2277
2278 (cells, current_cursor_pos, cursor_style)
2279 } else {
2280 return; };
2282
2283 let (scrollback_len, terminal_title) = if let Ok(term) = terminal.try_lock() {
2288 (term.scrollback_len(), term.get_title())
2289 } else {
2290 (
2291 self.cached_scrollback_len,
2292 self.cached_terminal_title.clone(),
2293 )
2294 };
2295
2296 self.cached_scrollback_len = scrollback_len;
2297 self.scroll_state
2298 .clamp_to_scrollback(self.cached_scrollback_len);
2299
2300 if self.config.allow_title_change
2303 && self.hovered_url.is_none()
2304 && terminal_title != self.cached_terminal_title
2305 && let Some(window) = &self.window
2306 {
2307 self.cached_terminal_title = terminal_title.clone();
2308 if terminal_title.is_empty() {
2309 window.set_title(&self.config.window_title);
2311 } else {
2312 window.set_title(&terminal_title);
2314 }
2315 }
2316
2317 let total_lines = visible_lines + scrollback_len;
2319
2320 let url_detect_start = std::time::Instant::now();
2323 let debug_url_detect_time = if !self.debug_cache_hit {
2324 self.detect_urls();
2326 url_detect_start.elapsed()
2327 } else {
2328 std::time::Duration::ZERO
2330 };
2331
2332 let url_underline_start = std::time::Instant::now();
2334 let mut cells = cells; self.apply_url_underlines(&mut cells, &renderer_size);
2336 let _debug_url_underline_time = url_underline_start.elapsed();
2337
2338 self.update_cursor_blink();
2340
2341 let render_start = std::time::Instant::now();
2342
2343 let mut debug_update_cells_time = std::time::Duration::ZERO;
2344 #[allow(unused_assignments)]
2345 let mut debug_graphics_time = std::time::Duration::ZERO;
2346 #[allow(unused_assignments)]
2347 let mut debug_actual_render_time = std::time::Duration::ZERO;
2348 let _ = &debug_actual_render_time;
2349 let mut pending_clipboard_action = ClipboardHistoryAction::None;
2351
2352 let show_scrollbar = self.should_show_scrollbar();
2353
2354 if let Some(renderer) = &mut self.renderer {
2355 if !self.debug_cache_hit {
2358 let t = std::time::Instant::now();
2359 renderer.update_cells(&cells);
2360 debug_update_cells_time = t.elapsed();
2361 }
2362
2363 if let (Some(pos), Some(opacity), Some(style)) =
2365 (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
2366 {
2367 renderer.update_cursor(pos, opacity, style);
2368 } else {
2369 renderer.clear_cursor();
2370 }
2371
2372 if self.settings_ui.visible {
2374 let ui_cfg = self.settings_ui.current_config().clone();
2375 if (ui_cfg.window_opacity - self.config.window_opacity).abs() > 1e-4 {
2376 log::info!(
2377 "Syncing live opacity from UI {:.3} (app {:.3})",
2378 ui_cfg.window_opacity,
2379 self.config.window_opacity
2380 );
2381 self.config.window_opacity = ui_cfg.window_opacity;
2382 }
2383
2384 renderer.update_opacity(self.config.window_opacity);
2385 self.last_applied_opacity = self.config.window_opacity;
2386 self.cached_cells = None;
2387 if let Some(window) = &self.window {
2388 window.request_redraw();
2389 }
2390 }
2391
2392 renderer.update_scrollbar(self.scroll_state.offset, visible_lines, total_lines);
2394
2395 let anim_start = std::time::Instant::now();
2397 if let Some(terminal) = &self.terminal {
2398 let terminal = terminal.blocking_lock();
2399 if terminal.update_animations() {
2400 if let Some(window) = &self.window {
2402 window.request_redraw();
2403 }
2404 }
2405 }
2406 let debug_anim_time = anim_start.elapsed();
2407
2408 let graphics_start = std::time::Instant::now();
2412 if let Some(terminal) = &self.terminal {
2413 let terminal = terminal.blocking_lock();
2414 let mut graphics = terminal.get_graphics_with_animations();
2415 let scrollback_len = terminal.scrollback_len();
2416
2417 let scrollback_graphics = terminal.get_scrollback_graphics();
2419 let scrollback_count = scrollback_graphics.len();
2420 graphics.extend(scrollback_graphics);
2421
2422 debug_info!(
2423 "APP",
2424 "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
2425 graphics.len(),
2426 scrollback_count,
2427 self.scroll_state.offset,
2428 scrollback_len
2429 );
2430 if let Err(e) = renderer.update_graphics(
2431 &graphics,
2432 self.scroll_state.offset,
2433 scrollback_len,
2434 visible_lines,
2435 ) {
2436 log::error!("Failed to update graphics: {}", e);
2437 }
2438 }
2439 debug_graphics_time = graphics_start.elapsed();
2440
2441 let visual_bell_intensity = if let Some(flash_start) = self.visual_bell_flash {
2443 const FLASH_DURATION_MS: u128 = 150;
2444 let elapsed = flash_start.elapsed().as_millis();
2445 if elapsed < FLASH_DURATION_MS {
2446 if let Some(window) = &self.window {
2448 window.request_redraw();
2449 }
2450 0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
2452 } else {
2453 self.visual_bell_flash = None;
2455 0.0
2456 }
2457 } else {
2458 0.0
2459 };
2460
2461 renderer.set_visual_bell_intensity(visual_bell_intensity);
2463
2464 let egui_start = std::time::Instant::now();
2466
2467 let show_fps = self.show_fps_overlay;
2469 let fps_value = self.fps_value;
2470 let frame_time_ms = if !self.debug_frame_times.is_empty() {
2471 let avg = self.debug_frame_times.iter().sum::<std::time::Duration>()
2472 / self.debug_frame_times.len() as u32;
2473 avg.as_secs_f64() * 1000.0
2474 } else {
2475 0.0
2476 };
2477
2478 let mut pending_config_update: Option<(
2480 Option<crate::config::Config>,
2481 Option<crate::config::Config>,
2482 Option<ShaderEditorResult>,
2483 )> = None;
2484
2485 let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
2486 (&self.egui_ctx, &mut self.egui_state)
2487 {
2488 let raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
2489
2490 let egui_output = egui_ctx.run(raw_input, |ctx| {
2491 if show_fps {
2493 egui::Area::new(egui::Id::new("fps_overlay"))
2494 .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
2495 .order(egui::Order::Foreground)
2496 .show(ctx, |ui| {
2497 egui::Frame::NONE
2498 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
2499 .inner_margin(egui::Margin::same(8))
2500 .corner_radius(4.0)
2501 .show(ui, |ui| {
2502 ui.style_mut().visuals.override_text_color =
2503 Some(egui::Color32::from_rgb(0, 255, 0));
2504 ui.label(
2505 egui::RichText::new(format!(
2506 "FPS: {:.1}\nFrame: {:.2}ms",
2507 fps_value, frame_time_ms
2508 ))
2509 .monospace()
2510 .size(14.0),
2511 );
2512 });
2513 });
2514 }
2515
2516 let settings_result = self.settings_ui.show(ctx);
2518 pending_config_update = Some(settings_result);
2519
2520 self.help_ui.show(ctx);
2522
2523 pending_clipboard_action = self.clipboard_history_ui.show(ctx);
2525 });
2526
2527 egui_state.handle_platform_output(
2530 self.window.as_ref().unwrap(),
2531 egui_output.platform_output.clone(),
2532 );
2533
2534 Some((egui_output, egui_ctx))
2535 } else {
2536 None
2537 };
2538
2539 if let Some((config_to_save, config_for_live_update, shader_apply)) =
2541 pending_config_update
2542 {
2543 if let Some(shader_result) = shader_apply {
2545 log::info!(
2546 "Applying shader from editor ({} bytes)",
2547 shader_result.source.len()
2548 );
2549 match renderer.reload_shader_from_source(&shader_result.source) {
2550 Ok(()) => {
2551 log::info!("Shader applied successfully from editor");
2552 self.settings_ui.clear_shader_error();
2553 }
2554 Err(e) => {
2555 let error_msg = format!("{:#}", e);
2556 log::error!("Shader compilation failed: {}", error_msg);
2557 self.settings_ui.set_shader_error(Some(error_msg));
2558 }
2559 }
2560 }
2561 if let Some(live_config) = config_for_live_update {
2563 let theme_changed = live_config.theme != self.config.theme;
2564 let shader_animation_changed =
2565 live_config.custom_shader_animation != self.config.custom_shader_animation;
2566 let shader_enabled_changed =
2567 live_config.custom_shader_enabled != self.config.custom_shader_enabled;
2568 let shader_path_changed =
2569 live_config.custom_shader != self.config.custom_shader;
2570 let shader_speed_changed = (live_config.custom_shader_animation_speed
2571 - self.config.custom_shader_animation_speed)
2572 .abs()
2573 > f32::EPSILON;
2574 let shader_full_content_changed = live_config.custom_shader_full_content
2575 != self.config.custom_shader_full_content;
2576 let shader_text_opacity_changed = (live_config.custom_shader_text_opacity
2577 - self.config.custom_shader_text_opacity)
2578 .abs()
2579 > f32::EPSILON;
2580 let _scrollbar_position_changed =
2581 live_config.scrollbar_position != self.config.scrollbar_position;
2582 let window_title_changed = live_config.window_title != self.config.window_title;
2583 let window_decorations_changed =
2584 live_config.window_decorations != self.config.window_decorations;
2585 let max_fps_changed = live_config.max_fps != self.config.max_fps;
2586 let cursor_style_changed = live_config.cursor_style != self.config.cursor_style;
2587 let bg_enabled_changed = live_config.background_image_enabled
2588 != self.config.background_image_enabled;
2589 let bg_path_changed =
2590 live_config.background_image != self.config.background_image;
2591 let bg_mode_changed =
2592 live_config.background_image_mode != self.config.background_image_mode;
2593 let bg_opacity_changed = (live_config.background_image_opacity
2594 - self.config.background_image_opacity)
2595 .abs()
2596 > f32::EPSILON;
2597 let font_changed = live_config.font_family != self.config.font_family
2598 || live_config.font_family_bold != self.config.font_family_bold
2599 || live_config.font_family_italic != self.config.font_family_italic
2600 || live_config.font_family_bold_italic
2601 != self.config.font_family_bold_italic
2602 || (live_config.font_size - self.config.font_size).abs() > f32::EPSILON
2603 || (live_config.line_spacing - self.config.line_spacing).abs()
2604 > f32::EPSILON
2605 || (live_config.char_spacing - self.config.char_spacing).abs()
2606 > f32::EPSILON;
2607 let padding_changed = (live_config.window_padding - self.config.window_padding)
2608 .abs()
2609 > f32::EPSILON;
2610 log::info!(
2611 "Applying live config update - opacity: {}{}{}",
2612 live_config.window_opacity,
2613 if theme_changed {
2614 " (theme changed)"
2615 } else {
2616 ""
2617 },
2618 if font_changed { " (font changed)" } else { "" }
2619 );
2620 self.config = live_config;
2621 self.scroll_state.last_activity = std::time::Instant::now();
2622
2623 if let Some(window) = &self.window {
2625 window.set_window_level(if self.config.window_always_on_top {
2627 winit::window::WindowLevel::AlwaysOnTop
2628 } else {
2629 winit::window::WindowLevel::Normal
2630 });
2631
2632 if window_title_changed {
2634 window.set_title(&self.config.window_title);
2635 log::info!("Updated window title to: {}", self.config.window_title);
2636 }
2637
2638 if window_decorations_changed {
2640 window.set_decorations(self.config.window_decorations);
2641 log::info!(
2642 "Updated window decorations: {}",
2643 self.config.window_decorations
2644 );
2645 }
2646
2647 window.request_redraw();
2649 }
2650
2651 if max_fps_changed {
2653 if let Some(old_task) = self.refresh_task.take() {
2655 old_task.abort();
2656 }
2657 if let Some(window) = &self.window {
2659 let window_clone = Arc::clone(window);
2660 let refresh_interval_ms = 1000 / self.config.max_fps.max(1); let handle = self.runtime.spawn(async move {
2662 let mut interval = tokio::time::interval(
2663 tokio::time::Duration::from_millis(refresh_interval_ms as u64),
2664 );
2665 loop {
2666 interval.tick().await;
2667 window_clone.request_redraw();
2668 }
2669 });
2670 self.refresh_task = Some(handle);
2671 log::info!(
2672 "Updated max_fps to {} ({}ms interval)",
2673 self.config.max_fps,
2674 refresh_interval_ms
2675 );
2676 }
2677 }
2678
2679 renderer.update_opacity(self.config.window_opacity);
2681 renderer.update_scrollbar_appearance(
2682 self.config.scrollbar_width,
2683 self.config.scrollbar_thumb_color,
2684 self.config.scrollbar_track_color,
2685 );
2686 if cursor_style_changed {
2689 if let Some(terminal_mgr) = &self.terminal
2692 && let Ok(term_mgr) = terminal_mgr.try_lock()
2693 {
2694 let terminal = term_mgr.terminal();
2696 if let Some(mut term) = terminal.try_lock() {
2697 use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
2699 let term_style = match self.config.cursor_style {
2700 crate::config::CursorStyle::Block => {
2701 TermCursorStyle::SteadyBlock
2702 }
2703 crate::config::CursorStyle::Underline => {
2704 TermCursorStyle::SteadyUnderline
2705 }
2706 crate::config::CursorStyle::Beam => TermCursorStyle::SteadyBar,
2707 };
2708 term.set_cursor_style(term_style);
2709 }
2710 }
2711
2712 self.cached_cells = None;
2714 self.last_cursor_pos = None;
2715 if let Some(window) = &self.window {
2716 window.request_redraw();
2717 }
2718 }
2719 if self.config.background_image_enabled {
2720 renderer
2721 .update_background_image_opacity(self.config.background_image_opacity);
2722 }
2723
2724 if bg_enabled_changed
2725 || bg_path_changed
2726 || bg_mode_changed
2727 || bg_opacity_changed
2728 {
2729 renderer.set_background_image_enabled(
2730 self.config.background_image_enabled,
2731 self.config.background_image.as_deref(),
2732 self.config.background_image_mode,
2733 self.config.background_image_opacity,
2734 );
2735 }
2736
2737 if shader_animation_changed
2738 || shader_enabled_changed
2739 || shader_path_changed
2740 || shader_speed_changed
2741 || shader_full_content_changed
2742 || shader_text_opacity_changed
2743 {
2744 match renderer.set_custom_shader_enabled(
2745 self.config.custom_shader_enabled,
2746 self.config.custom_shader.as_deref(),
2747 self.config.window_opacity,
2748 self.config.custom_shader_text_opacity,
2749 self.config.custom_shader_animation,
2750 self.config.custom_shader_animation_speed,
2751 self.config.custom_shader_full_content,
2752 ) {
2753 Ok(()) => {
2754 self.settings_ui.clear_shader_error();
2755 }
2756 Err(error_msg) => {
2757 log::error!("Shader compilation failed: {}", error_msg);
2758 self.settings_ui.set_shader_error(Some(error_msg));
2759 }
2760 }
2761 }
2762
2763 if theme_changed {
2765 if let Some(terminal) = &self.terminal
2766 && let Ok(mut term) = terminal.try_lock()
2767 {
2768 term.set_theme(self.config.load_theme());
2769 log::info!("Applied live theme change: {}", self.config.theme);
2770 }
2771 self.cached_cells = None;
2773 if let Some(window) = &self.window {
2774 window.request_redraw();
2775 }
2776 }
2777
2778 if font_changed {
2779 self.pending_font_rebuild = true;
2781 log::info!("Queued renderer rebuild for font change");
2782 }
2783
2784 if padding_changed {
2786 if let Some((cols, rows)) =
2787 renderer.update_window_padding(self.config.window_padding)
2788 {
2789 let cell_width = renderer.cell_width();
2791 let cell_height = renderer.cell_height();
2792 let width_px = (cols as f32 * cell_width) as usize;
2793 let height_px = (rows as f32 * cell_height) as usize;
2794
2795 if let Some(terminal) = &self.terminal
2796 && let Ok(mut term) = terminal.try_lock()
2797 {
2798 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
2799 log::info!(
2800 "Resized terminal to {}x{} due to padding change",
2801 cols,
2802 rows
2803 );
2804 }
2805 }
2806 log::info!("Updated window padding to {}", self.config.window_padding);
2807 }
2808
2809 self.cached_cells = None;
2811
2812 self.last_applied_opacity = self.config.window_opacity;
2814
2815 if let Some(window) = &self.window {
2817 window.request_redraw();
2818 }
2819 }
2820
2821 if let Some(new_config) = config_to_save {
2823 if let Err(e) = new_config.save() {
2824 log::error!("Failed to save config: {}", e);
2825 } else {
2826 log::info!("Configuration saved successfully");
2827 log::info!(
2828 " Bell settings: sound={}, visual={}, desktop={}",
2829 new_config.notification_bell_sound,
2830 new_config.notification_bell_visual,
2831 new_config.notification_bell_desktop
2832 );
2833 self.settings_ui.update_config(new_config);
2835 }
2836 }
2837 }
2838 let debug_egui_time = egui_start.elapsed();
2839
2840 let avg_frame_time = if !self.debug_frame_times.is_empty() {
2842 self.debug_frame_times.iter().sum::<std::time::Duration>()
2843 / self.debug_frame_times.len() as u32
2844 } else {
2845 std::time::Duration::ZERO
2846 };
2847 let fps = if avg_frame_time.as_secs_f64() > 0.0 {
2848 1.0 / avg_frame_time.as_secs_f64()
2849 } else {
2850 0.0
2851 };
2852
2853 self.fps_value = fps;
2855
2856 if self.debug_frame_times.len() >= 60 {
2858 log::info!(
2859 "PERF: FPS={:.1} Frame={:.2}ms CellGen={:.2}ms({}) URLDetect={:.2}ms Anim={:.2}ms Graphics={:.2}ms egui={:.2}ms UpdateCells={:.2}ms ActualRender={:.2}ms Total={:.2}ms Cells={} Gen={} Cache={}",
2860 fps,
2861 avg_frame_time.as_secs_f64() * 1000.0,
2862 self.debug_cell_gen_time.as_secs_f64() * 1000.0,
2863 if self.debug_cache_hit { "HIT" } else { "MISS" },
2864 debug_url_detect_time.as_secs_f64() * 1000.0,
2865 debug_anim_time.as_secs_f64() * 1000.0,
2866 debug_graphics_time.as_secs_f64() * 1000.0,
2867 debug_egui_time.as_secs_f64() * 1000.0,
2868 debug_update_cells_time.as_secs_f64() * 1000.0,
2869 debug_actual_render_time.as_secs_f64() * 1000.0,
2870 self.debug_render_time.as_secs_f64() * 1000.0,
2871 cells.len(),
2872 self.last_generation,
2873 if self.cached_cells.is_some() {
2874 "YES"
2875 } else {
2876 "NO"
2877 }
2878 );
2879 }
2880
2881 let actual_render_start = std::time::Instant::now();
2883 match renderer.render(egui_data, self.settings_ui.visible, show_scrollbar) {
2884 Ok(rendered) => {
2885 if !rendered {
2886 log::trace!("Skipped rendering - no changes");
2887 }
2888 }
2889 Err(e) => {
2890 if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
2893 match surface_error {
2894 SurfaceError::Outdated | SurfaceError::Lost => {
2895 log::warn!(
2896 "Surface error detected ({:?}), reconfiguring...",
2897 surface_error
2898 );
2899 self.force_surface_reconfigure();
2900 }
2901 SurfaceError::Timeout => {
2902 log::warn!("Surface timeout, will retry next frame");
2903 if let Some(window) = &self.window {
2904 window.request_redraw();
2905 }
2906 }
2907 SurfaceError::OutOfMemory => {
2908 log::error!("Surface out of memory: {:?}", surface_error);
2909 }
2910 _ => {
2911 log::error!("Surface error: {:?}", surface_error);
2912 }
2913 }
2914 } else {
2915 log::error!("Render error: {}", e);
2916 }
2917 }
2918 }
2919 debug_actual_render_time = actual_render_start.elapsed();
2920 let _ = debug_actual_render_time;
2921
2922 self.debug_render_time = render_start.elapsed();
2923 }
2924
2925 match pending_clipboard_action {
2928 ClipboardHistoryAction::Paste(content) => {
2929 self.paste_text(&content);
2930 }
2931 ClipboardHistoryAction::ClearAll => {
2932 if let Some(terminal) = &self.terminal
2933 && let Ok(term) = terminal.try_lock()
2934 {
2935 term.clear_all_clipboard_history();
2936 log::info!("Cleared all clipboard history");
2937 }
2938 self.clipboard_history_ui.update_entries(Vec::new());
2939 }
2940 ClipboardHistoryAction::ClearSlot(slot) => {
2941 if let Some(terminal) = &self.terminal
2942 && let Ok(term) = terminal.try_lock()
2943 {
2944 term.clear_clipboard_history(slot);
2945 log::info!("Cleared clipboard history for slot {:?}", slot);
2946 }
2947 }
2948 ClipboardHistoryAction::None => {}
2949 }
2950
2951 let absolute_total = absolute_start.elapsed();
2952 if absolute_total.as_millis() > 10 {
2953 log::debug!(
2954 "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
2955 absolute_total.as_secs_f64() * 1000.0
2956 );
2957 }
2958 }
2959
2960 fn check_notifications(&mut self) {
2961 if let Some(terminal) = &self.terminal
2962 && let Ok(term) = terminal.try_lock()
2963 {
2964 if term.has_notifications() {
2966 let notifications = term.take_notifications();
2967 for notif in notifications {
2968 self.deliver_notification(¬if.title, ¬if.message);
2969 }
2970 }
2971 }
2972 }
2973
2974 fn check_bell(&mut self) {
2975 if self.config.notification_bell_sound == 0
2977 && !self.config.notification_bell_visual
2978 && !self.config.notification_bell_desktop
2979 {
2980 return;
2981 }
2982
2983 if let Some(terminal) = &self.terminal
2984 && let Ok(term) = terminal.try_lock()
2985 {
2986 let current_bell_count = term.bell_count();
2987
2988 if current_bell_count > self.last_bell_count {
2989 let bell_events = current_bell_count - self.last_bell_count;
2991 log::info!("🔔 Bell event detected ({} bell(s))", bell_events);
2992 log::info!(
2993 " Config: sound={}, visual={}, desktop={}",
2994 self.config.notification_bell_sound,
2995 self.config.notification_bell_visual,
2996 self.config.notification_bell_desktop
2997 );
2998
2999 if self.config.notification_bell_sound > 0 {
3001 if let Some(audio_bell) = &self.audio_bell {
3002 log::info!(
3003 " Playing audio bell at {}% volume",
3004 self.config.notification_bell_sound
3005 );
3006 audio_bell.play(self.config.notification_bell_sound);
3007 } else {
3008 log::warn!(" Audio bell requested but not initialized");
3009 }
3010 } else {
3011 log::debug!(" Audio bell disabled (volume=0)");
3012 }
3013
3014 if self.config.notification_bell_visual {
3016 log::info!(" Triggering visual bell flash");
3017 self.visual_bell_flash = Some(std::time::Instant::now());
3018 if let Some(window) = &self.window {
3020 window.request_redraw();
3021 }
3022 } else {
3023 log::debug!(" Visual bell disabled");
3024 }
3025
3026 if self.config.notification_bell_desktop {
3028 log::info!(" Sending desktop notification");
3029 let message = if bell_events == 1 {
3030 "Terminal bell".to_string()
3031 } else {
3032 format!("Terminal bell ({} events)", bell_events)
3033 };
3034 self.deliver_notification("Terminal", &message);
3035 } else {
3036 log::debug!(" Desktop notification disabled");
3037 }
3038
3039 self.last_bell_count = current_bell_count;
3040 }
3041 }
3042 }
3043
3044 fn take_screenshot(&self) {
3045 log::info!("Taking screenshot...");
3046
3047 if let Some(terminal) = &self.terminal {
3048 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
3050 let format = &self.config.screenshot_format;
3051 let filename = format!("par-term_screenshot_{}.{}", timestamp, format);
3052
3053 if let Some(home_dir) = dirs::home_dir() {
3055 let screenshot_dir = home_dir.join("par-term-screenshots");
3056 if !screenshot_dir.exists()
3057 && let Err(e) = std::fs::create_dir_all(&screenshot_dir)
3058 {
3059 log::error!("Failed to create screenshot directory: {}", e);
3060 self.deliver_notification(
3061 "Screenshot Error",
3062 &format!("Failed to create directory: {}", e),
3063 );
3064 return;
3065 }
3066
3067 let path = screenshot_dir.join(&filename);
3068 let path_str = path.to_string_lossy().to_string();
3069
3070 let terminal_clone = Arc::clone(terminal);
3072 let format_clone = format.clone();
3073
3074 let result = std::thread::spawn(move || {
3076 if let Ok(term) = terminal_clone.try_lock() {
3077 term.screenshot_to_file(&path, &format_clone, 0)
3079 } else {
3080 Err(anyhow::anyhow!("Failed to lock terminal"))
3081 }
3082 })
3083 .join();
3084
3085 match result {
3086 Ok(Ok(())) => {
3087 log::info!("Screenshot saved to: {}", path_str);
3088 self.deliver_notification(
3089 "Screenshot Saved",
3090 &format!("Saved to: {}", path_str),
3091 );
3092 }
3093 Ok(Err(e)) => {
3094 log::error!("Failed to save screenshot: {}", e);
3095 self.deliver_notification(
3096 "Screenshot Error",
3097 &format!("Failed to save: {}", e),
3098 );
3099 }
3100 Err(e) => {
3101 log::error!("Screenshot thread panicked: {:?}", e);
3102 self.deliver_notification("Screenshot Error", "Screenshot thread failed");
3103 }
3104 }
3105 } else {
3106 log::error!("Failed to get home directory");
3107 self.deliver_notification("Screenshot Error", "Failed to get home directory");
3108 }
3109 } else {
3110 log::warn!("No terminal available for screenshot");
3111 self.deliver_notification("Screenshot Error", "No terminal available");
3112 }
3113 }
3114
3115 fn toggle_recording(&mut self) {
3118 log::warn!("Recording functionality not yet available in core library");
3119 self.deliver_notification(
3120 "Recording Not Available",
3121 "Recording APIs are not yet implemented in the core library",
3122 );
3123 }
3124
3125 fn deliver_notification(&self, title: &str, message: &str) {
3271 if !title.is_empty() {
3273 log::info!("=== Notification: {} ===", title);
3274 log::info!("{}", message);
3275 log::info!("===========================");
3276 } else {
3277 log::info!("=== Notification ===");
3278 log::info!("{}", message);
3279 log::info!("===================");
3280 }
3281
3282 #[cfg(not(target_os = "macos"))]
3284 {
3285 use notify_rust::Notification;
3286 let notification_title = if !title.is_empty() {
3287 title
3288 } else {
3289 "Terminal Notification"
3290 };
3291
3292 if let Err(e) = Notification::new()
3293 .summary(notification_title)
3294 .body(message)
3295 .timeout(notify_rust::Timeout::Milliseconds(3000))
3296 .show()
3297 {
3298 log::warn!("Failed to send desktop notification: {}", e);
3299 }
3300 }
3301
3302 #[cfg(target_os = "macos")]
3303 {
3304 let notification_title = if !title.is_empty() {
3306 title
3307 } else {
3308 "Terminal Notification"
3309 };
3310
3311 let escaped_title = notification_title.replace('"', "\\\"");
3313 let escaped_message = message.replace('"', "\\\"");
3314
3315 let script = format!(
3317 r#"display notification "{}" with title "{}""#,
3318 escaped_message, escaped_title
3319 );
3320
3321 if let Err(e) = std::process::Command::new("osascript")
3322 .arg("-e")
3323 .arg(&script)
3324 .output()
3325 {
3326 log::warn!("Failed to send macOS desktop notification: {}", e);
3327 }
3328 }
3329 }
3330
3331 fn update_window_title_with_shell_integration(&self) {
3334 if self.scroll_state.offset != 0 {
3336 return;
3337 }
3338
3339 if self.hovered_url.is_some() {
3341 return;
3342 }
3343
3344 let window = if let Some(w) = &self.window {
3346 w
3347 } else {
3348 return;
3349 };
3350
3351 let terminal = if let Some(t) = &self.terminal {
3353 t
3354 } else {
3355 return;
3356 };
3357
3358 if let Ok(term) = terminal.try_lock() {
3360 let mut title_parts = vec![self.config.window_title.clone()];
3361
3362 if let Some(cwd) = term.shell_integration_cwd() {
3364 let abbreviated_cwd = if let Some(home) = dirs::home_dir() {
3366 cwd.replace(&home.to_string_lossy().to_string(), "~")
3367 } else {
3368 cwd
3369 };
3370 title_parts.push(format!("({})", abbreviated_cwd));
3371 }
3372
3373 if let Some(exit_code) = term.shell_integration_exit_code()
3375 && exit_code != 0
3376 {
3377 title_parts.push(format!("[Exit: {}]", exit_code));
3378 }
3379
3380 if self.is_recording {
3382 title_parts.push("[RECORDING]".to_string());
3383 }
3384
3385 let title = title_parts.join(" ");
3387 window.set_title(&title);
3388 }
3389 }
3390}
3391
3392impl ApplicationHandler for AppState {
3393 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
3394 if self.window.is_none() {
3395 let mut window_attrs = Window::default_attributes()
3396 .with_title(&self.config.window_title)
3397 .with_inner_size(winit::dpi::LogicalSize::new(
3398 self.config.window_width,
3399 self.config.window_height,
3400 ))
3401 .with_decorations(self.config.window_decorations);
3402
3403 let icon_bytes = include_bytes!("../assets/icon.png");
3405 if let Ok(icon_image) = image::load_from_memory(icon_bytes) {
3406 let rgba = icon_image.to_rgba8();
3407 let (width, height) = rgba.dimensions();
3408 if let Ok(icon) = winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
3409 window_attrs = window_attrs.with_window_icon(Some(icon));
3410 log::info!("Window icon set ({}x{})", width, height);
3411 } else {
3412 log::warn!("Failed to create window icon from RGBA data");
3413 }
3414 } else {
3415 log::warn!("Failed to load embedded icon image");
3416 }
3417
3418 if self.config.window_always_on_top {
3420 window_attrs =
3421 window_attrs.with_window_level(winit::window::WindowLevel::AlwaysOnTop);
3422 log::info!("Window always-on-top enabled");
3423 }
3424
3425 window_attrs = window_attrs.with_transparent(true);
3428 log::info!(
3429 "Window transparency enabled (opacity: {})",
3430 self.config.window_opacity
3431 );
3432
3433 match event_loop.create_window(window_attrs) {
3434 Ok(window) => {
3435 let runtime = Arc::clone(&self.runtime);
3437 if let Err(e) = runtime.block_on(self.initialize_async(window)) {
3438 log::error!("Failed to initialize: {}", e);
3439 event_loop.exit();
3440 }
3441 }
3442 Err(e) => {
3443 log::error!("Failed to create window: {}", e);
3444 event_loop.exit();
3445 }
3446 }
3447 }
3448 }
3449
3450 fn window_event(
3451 &mut self,
3452 event_loop: &ActiveEventLoop,
3453 _window_id: WindowId,
3454 event: WindowEvent,
3455 ) {
3456 use winit::keyboard::{Key, NamedKey};
3457
3458 if let WindowEvent::KeyboardInput {
3460 event: key_event, ..
3461 } = &event
3462 {
3463 match &key_event.logical_key {
3464 Key::Character(s) => {
3465 log::trace!(
3466 "window_event: Character '{}', state={:?}",
3467 s,
3468 key_event.state
3469 );
3470 }
3471 Key::Named(NamedKey::Space) => {
3472 log::debug!("🔔 SPACE EVENT: state={:?}", key_event.state);
3473 }
3474 Key::Named(named) => {
3475 log::trace!(
3476 "window_event: Named key {:?}, state={:?}",
3477 named,
3478 key_event.state
3479 );
3480 }
3481 other => {
3482 log::trace!(
3483 "window_event: Other key {:?}, state={:?}",
3484 other,
3485 key_event.state
3486 );
3487 }
3488 }
3489 }
3490
3491 let egui_consumed =
3493 if let (Some(egui_state), Some(window)) = (&mut self.egui_state, &self.window) {
3494 let event_response = egui_state.on_window_event(window, &event);
3495 event_response.consumed
3496 } else {
3497 false
3498 };
3499
3500 if egui_consumed
3502 && !self.settings_ui.visible
3503 && let WindowEvent::KeyboardInput {
3504 event: key_event, ..
3505 } = &event
3506 && let Key::Named(NamedKey::Space) = &key_event.logical_key
3507 {
3508 log::debug!("egui tried to consume Space (UI closed, ignoring)");
3509 }
3510
3511 let any_ui_visible =
3514 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
3515 if egui_consumed
3516 && any_ui_visible
3517 && !matches!(
3518 event,
3519 WindowEvent::CloseRequested | WindowEvent::RedrawRequested
3520 )
3521 {
3522 if let WindowEvent::KeyboardInput {
3523 event: key_event, ..
3524 } = &event
3525 {
3526 match &key_event.logical_key {
3527 Key::Named(NamedKey::Space) => {
3528 log::debug!("egui consumed Space while UI panel is visible")
3529 }
3530 Key::Named(_) => {
3531 log::debug!("egui consumed named key while UI panel is visible")
3532 }
3533 _ => {}
3534 }
3535 }
3536 return;
3537 }
3538
3539 match event {
3540 WindowEvent::CloseRequested => {
3541 log::info!("Close requested, cleaning up and exiting");
3542 self.is_shutting_down = true;
3544 if let Some(task) = self.refresh_task.take() {
3546 task.abort();
3547 log::info!("Refresh task aborted");
3548 }
3549 event_loop.exit();
3550 }
3551
3552 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
3553 if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
3554 log::info!(
3555 "Scale factor changed to {} (display change detected)",
3556 scale_factor
3557 );
3558
3559 let size = window.inner_size();
3560 let (cols, rows) = renderer.handle_scale_factor_change(scale_factor, size);
3561
3562 renderer.reconfigure_surface();
3565
3566 let cell_width = renderer.cell_width();
3568 let cell_height = renderer.cell_height();
3569 let width_px = (cols as f32 * cell_width) as usize;
3570 let height_px = (rows as f32 * cell_height) as usize;
3571
3572 if let Some(terminal) = &self.terminal
3574 && let Ok(mut term) = terminal.try_lock()
3575 {
3576 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
3577 }
3578
3579 #[cfg(target_os = "macos")]
3581 {
3582 if let Err(e) =
3583 crate::macos_metal::configure_metal_layer_for_performance(window)
3584 {
3585 log::warn!(
3586 "Failed to reconfigure Metal layer after display change: {}",
3587 e
3588 );
3589 }
3590 }
3591
3592 window.request_redraw();
3594 }
3595 }
3596
3597 WindowEvent::Moved(_) => {
3599 if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
3600 log::debug!(
3601 "Window moved - reconfiguring surface for potential display change"
3602 );
3603
3604 renderer.reconfigure_surface();
3607
3608 #[cfg(target_os = "macos")]
3610 {
3611 if let Err(e) =
3612 crate::macos_metal::configure_metal_layer_for_performance(window)
3613 {
3614 log::warn!(
3615 "Failed to reconfigure Metal layer after window move: {}",
3616 e
3617 );
3618 }
3619 }
3620
3621 window.request_redraw();
3623 }
3624 }
3625
3626 WindowEvent::Resized(physical_size) => {
3627 if let Some(renderer) = &mut self.renderer {
3628 let (cols, rows) = renderer.resize(physical_size);
3629
3630 let cell_width = renderer.cell_width();
3632 let cell_height = renderer.cell_height();
3633 let width_px = (cols as f32 * cell_width) as usize;
3634 let height_px = (rows as f32 * cell_height) as usize;
3635
3636 if let Some(terminal) = &self.terminal
3641 && let Ok(mut term) = terminal.try_lock()
3642 {
3643 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
3644 self.cached_scrollback_len = term.scrollback_len();
3645
3646 let total_lines = rows + self.cached_scrollback_len;
3648 renderer.update_scrollbar(self.scroll_state.offset, rows, total_lines);
3649 }
3650
3651 self.cached_cells = None;
3653 }
3654 }
3655
3656 WindowEvent::KeyboardInput { event, .. } => {
3657 self.handle_key_event(event, event_loop);
3658 }
3659
3660 WindowEvent::ModifiersChanged(modifiers) => {
3661 self.input_handler.update_modifiers(modifiers);
3662 }
3663
3664 WindowEvent::MouseWheel { delta, .. } => {
3665 if !self.is_egui_using_pointer() {
3667 self.handle_mouse_wheel(delta);
3668 }
3669 }
3670
3671 WindowEvent::MouseInput { button, state, .. } => {
3672 if !self.is_egui_using_pointer() {
3674 self.handle_mouse_button(button, state);
3675 }
3676 }
3677
3678 WindowEvent::CursorMoved { position, .. } => {
3679 if !self.is_egui_using_pointer() {
3681 self.handle_mouse_move((position.x, position.y));
3682 }
3683 }
3684
3685 WindowEvent::RedrawRequested => {
3686 if self.is_shutting_down {
3688 return;
3689 }
3690
3691 if self.config.exit_on_shell_exit
3693 && let Some(terminal) = &self.terminal
3694 && let Ok(term) = terminal.try_lock()
3695 && !term.is_running()
3696 {
3697 log::info!("Shell has exited, closing terminal");
3698 self.is_shutting_down = true;
3700 if let Some(task) = self.refresh_task.take() {
3702 task.abort();
3703 log::info!("Refresh task aborted");
3704 }
3705 event_loop.exit();
3706 return;
3707 }
3708
3709 self.render();
3710 }
3711
3712 _ => {}
3713 }
3714 }
3715
3716 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
3717 if self.is_shutting_down {
3719 return;
3720 }
3721
3722 self.check_notifications();
3724
3725 self.check_bell();
3727
3728 self.update_window_title_with_shell_integration();
3730
3731 let now = std::time::Instant::now();
3737 let mut next_wake = now + std::time::Duration::from_secs(1); if self.config.cursor_blink {
3742 if self.cursor_blink_timer.is_none() {
3743 let blink_interval =
3744 std::time::Duration::from_millis(self.config.cursor_blink_interval);
3745 self.cursor_blink_timer = Some(now + blink_interval);
3746 }
3747
3748 if let Some(next_blink) = self.cursor_blink_timer {
3749 if now >= next_blink {
3750 self.needs_redraw = true;
3752 let blink_interval =
3753 std::time::Duration::from_millis(self.config.cursor_blink_interval);
3754 self.cursor_blink_timer = Some(now + blink_interval);
3755 } else if next_blink < next_wake {
3756 next_wake = next_blink;
3758 }
3759 }
3760 }
3761
3762 if self.scroll_state.animation_start.is_some() {
3765 self.needs_redraw = true;
3766 let next_frame = now + std::time::Duration::from_millis(16);
3767 if next_frame < next_wake {
3768 next_wake = next_frame;
3769 }
3770 }
3771
3772 if self.visual_bell_flash.is_some() {
3775 self.needs_redraw = true;
3776 let next_frame = now + std::time::Duration::from_millis(16);
3777 if next_frame < next_wake {
3778 next_wake = next_frame;
3779 }
3780 }
3781
3782 if (self.is_selecting || self.selection.is_some() || self.scroll_state.dragging)
3785 && self.mouse_button_pressed
3786 {
3787 self.needs_redraw = true;
3788 }
3789
3790 if let Some(renderer) = &self.renderer
3793 && renderer.needs_continuous_render()
3794 {
3795 self.needs_redraw = true;
3796 let next_frame = now + std::time::Duration::from_millis(16);
3797 if next_frame < next_wake {
3798 next_wake = next_frame;
3799 }
3800 }
3801
3802 if self.needs_redraw
3805 && let Some(window) = &self.window
3806 {
3807 window.request_redraw();
3808 self.needs_redraw = false;
3809 }
3810
3811 event_loop.set_control_flow(ControlFlow::WaitUntil(next_wake));
3813 }
3814}
3815
3816impl Drop for AppState {
3817 fn drop(&mut self) {
3818 log::info!("Shutting down application");
3819
3820 self.is_shutting_down = true;
3822
3823 if let Some(handle) = self.refresh_task.take() {
3825 handle.abort();
3826 log::info!("Refresh task aborted");
3827
3828 std::thread::sleep(std::time::Duration::from_millis(100));
3830 }
3831
3832 if let Some(terminal) = &self.terminal {
3834 let killed = if let Ok(mut term) = terminal.try_lock() {
3836 if term.is_running() {
3837 log::info!("Killing PTY process during shutdown");
3838 match term.kill() {
3839 Ok(()) => {
3840 log::info!("PTY process killed successfully");
3841 true
3842 }
3843 Err(e) => {
3844 log::warn!("Failed to kill PTY process: {:?}", e);
3845 false
3846 }
3847 }
3848 } else {
3849 log::info!("PTY process already stopped");
3850 true
3851 }
3852 } else {
3853 log::warn!("Could not acquire terminal lock to kill PTY during shutdown");
3854 false
3855 };
3856
3857 if killed {
3859 std::thread::sleep(std::time::Duration::from_millis(100));
3860 }
3861 }
3862
3863 if let Some(terminal) = self.terminal.take() {
3865 let timeout = std::time::Duration::from_millis(500);
3867 let start = std::time::Instant::now();
3868
3869 loop {
3870 if let Ok(_term) = terminal.try_lock() {
3871 log::info!("Terminal lock acquired for cleanup");
3872 break;
3874 } else if start.elapsed() >= timeout {
3875 log::warn!(
3876 "Could not acquire terminal lock within timeout during shutdown, forcing cleanup"
3877 );
3878 break;
3880 }
3881 std::thread::sleep(std::time::Duration::from_millis(10));
3882 }
3883 }
3885
3886 log::info!("Application shutdown complete");
3887 }
3888}