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::{CursorShaderEditorResult, 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 mut 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 self.config.cursor_shader.as_deref(),
255 self.config.cursor_shader_enabled,
256 self.config.cursor_shader_animation,
257 self.config.cursor_shader_animation_speed,
258 ))?;
259
260 let (cols, rows) = renderer.grid_size();
261 let cell_width = renderer.cell_width();
262 let cell_height = renderer.cell_height();
263 let width_px = (cols as f32 * cell_width) as usize;
264 let height_px = (rows as f32 * cell_height) as usize;
265
266 if let Some(terminal) = &self.terminal
267 && let Ok(mut term) = terminal.try_lock()
268 {
269 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
270 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
271 term.set_theme(self.config.load_theme());
272 }
273
274 renderer.update_cursor_shader_config(
276 self.config.cursor_shader_color,
277 self.config.cursor_shader_trail_duration,
278 self.config.cursor_shader_glow_radius,
279 self.config.cursor_shader_glow_intensity,
280 );
281
282 renderer.update_cursor_color(self.config.cursor_color);
284
285 self.renderer = Some(renderer);
286 self.cached_cells = None;
287 self.needs_redraw = true;
288
289 let previous_memory = self
294 .egui_ctx
295 .as_ref()
296 .map(|ctx| ctx.memory(|mem| mem.clone()));
297
298 let scale_factor = window.scale_factor() as f32;
299 let egui_ctx = egui::Context::default();
300 if let Some(memory) = previous_memory {
301 egui_ctx.memory_mut(|mem| *mem = memory);
302 }
303 let egui_state = egui_winit::State::new(
304 egui_ctx.clone(),
305 egui::ViewportId::ROOT,
306 &window,
307 Some(scale_factor),
308 None,
309 None,
310 );
311 self.egui_ctx = Some(egui_ctx);
312 self.egui_state = Some(egui_state);
313
314 if let Some(window) = &self.window {
315 window.request_redraw();
316 }
317
318 Ok(())
319 }
320
321 async fn initialize_async(&mut self, window: Window) -> Result<()> {
322 window.set_ime_allowed(true);
324 log::debug!("IME enabled for character input");
325
326 let window = Arc::new(window);
327
328 let egui_ctx = egui::Context::default();
330 let egui_state = egui_winit::State::new(
331 egui_ctx.clone(),
332 egui::ViewportId::ROOT,
333 &window,
334 Some(window.scale_factor() as f32),
335 None,
336 None, );
338 self.egui_ctx = Some(egui_ctx);
339 self.egui_state = Some(egui_state);
340
341 let font_family = if self.config.font_family.is_empty() {
343 None
344 } else {
345 Some(self.config.font_family.as_str())
346 };
347 let theme = self.config.load_theme();
348 let mut renderer = Renderer::new(
349 Arc::clone(&window),
350 font_family,
351 self.config.font_family_bold.as_deref(),
352 self.config.font_family_italic.as_deref(),
353 self.config.font_family_bold_italic.as_deref(),
354 &self.config.font_ranges,
355 self.config.font_size,
356 self.config.window_padding,
357 self.config.line_spacing,
358 self.config.char_spacing,
359 &self.config.scrollbar_position,
360 self.config.scrollbar_width,
361 self.config.scrollbar_thumb_color,
362 self.config.scrollbar_track_color,
363 self.config.enable_text_shaping,
364 self.config.enable_ligatures,
365 self.config.enable_kerning,
366 self.config.vsync_mode,
367 self.config.window_opacity,
368 theme.background.as_array(),
369 self.config.background_image.as_deref(),
370 self.config.background_image_enabled,
371 self.config.background_image_mode,
372 self.config.background_image_opacity,
373 self.config.custom_shader.as_deref(),
374 self.config.custom_shader_enabled,
375 self.config.custom_shader_animation,
376 self.config.custom_shader_animation_speed,
377 self.config.custom_shader_text_opacity,
378 self.config.custom_shader_full_content,
379 self.config.cursor_shader.as_deref(),
381 self.config.cursor_shader_enabled,
382 self.config.cursor_shader_animation,
383 self.config.cursor_shader_animation_speed,
384 )
385 .await?;
386
387 #[cfg(target_os = "macos")]
392 {
393 if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
394 log::warn!("Failed to configure Metal layer: {}", e);
395 log::warn!(
396 "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
397 );
398 }
399 if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
401 log::warn!("Failed to set initial Metal layer opacity: {}", e);
402 }
403 }
404
405 let mut terminal = TerminalManager::new_with_scrollback(
407 self.config.cols,
408 self.config.rows,
409 self.config.scrollback_lines,
410 )?;
411
412 terminal.set_theme(self.config.load_theme());
414
415 terminal.set_max_clipboard_sync_events(self.config.clipboard_max_sync_events);
417 terminal.set_max_clipboard_event_bytes(self.config.clipboard_max_event_bytes);
418
419 let (renderer_cols, renderer_rows) = renderer.grid_size();
421 log::info!(
422 "Initial terminal dimensions: {}x{}",
423 renderer_cols,
424 renderer_rows
425 );
426
427 let cell_width = renderer.cell_width();
430 let cell_height = renderer.cell_height();
431 let width_px = (renderer_cols as f32 * cell_width) as usize;
432 let height_px = (renderer_rows as f32 * cell_height) as usize;
433 terminal.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px)?;
434 log::info!(
435 "Initial terminal pixel dimensions: {}x{} px",
436 width_px,
437 height_px
438 );
439
440 let working_dir = self.config.working_directory.as_deref();
442 let shell_env = self.config.shell_env.as_ref();
443
444 let (shell_cmd, mut shell_args) = if let Some(ref custom) = self.config.custom_shell {
446 (custom.clone(), self.config.shell_args.clone())
447 } else {
448 #[cfg(target_os = "windows")]
449 {
450 ("powershell.exe".to_string(), None)
451 }
452 #[cfg(not(target_os = "windows"))]
453 {
454 (
455 std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()),
456 None,
457 )
458 }
459 };
460
461 #[cfg(not(target_os = "windows"))]
464 if self.config.login_shell {
465 let args = shell_args.get_or_insert_with(Vec::new);
466 if !args.iter().any(|a| a == "-l" || a == "--login") {
468 args.insert(0, "-l".to_string());
469 }
470 }
471
472 let shell_args_deref = shell_args.as_deref();
473 terminal.spawn_custom_shell_with_dir(
474 &shell_cmd,
475 shell_args_deref,
476 working_dir,
477 shell_env,
478 )?;
479
480 let cell_width = renderer.cell_width() as u32;
482 let cell_height = renderer.cell_height() as u32;
483 log::info!("Setting cell dimensions: {}x{}", cell_width, cell_height);
484 terminal.set_cell_dimensions(cell_width, cell_height);
485
486 renderer.update_cursor_shader_config(
488 self.config.cursor_shader_color,
489 self.config.cursor_shader_trail_duration,
490 self.config.cursor_shader_glow_radius,
491 self.config.cursor_shader_glow_intensity,
492 );
493
494 renderer.update_cursor_color(self.config.cursor_color);
496
497 self.window = Some(Arc::clone(&window));
498 self.renderer = Some(renderer);
499 self.terminal = Some(Arc::new(Mutex::new(terminal)));
500
501 let window_clone = Arc::clone(&window);
503 let terminal_clone = Arc::clone(self.terminal.as_ref().unwrap());
504 let max_fps = self.config.max_fps.max(1);
505 let refresh_interval_ms = 1000 / max_fps;
506
507 let handle = self.runtime.spawn(async move {
508 let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(
509 refresh_interval_ms as u64,
510 ));
511 let mut last_gen = 0;
513
514 loop {
515 interval.tick().await;
516
517 let should_redraw = if let Ok(term) = terminal_clone.try_lock() {
520 let current_gen = term.update_generation();
521 if current_gen > last_gen {
522 last_gen = current_gen;
523 true
524 } else {
525 term.has_updates()
527 }
528 } else {
529 false
531 };
532
533 if should_redraw {
534 window_clone.request_redraw();
535 }
536 }
537 });
538 self.refresh_task = Some(handle);
539
540 Ok(())
541 }
542
543 fn handle_key_event(&mut self, event: KeyEvent, event_loop: &ActiveEventLoop) {
544 use winit::event::ElementState;
545 use winit::keyboard::{Key, NamedKey};
546
547 let any_ui_visible =
549 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
550
551 if any_ui_visible {
554 let is_ui_control_key = matches!(
555 event.logical_key,
556 Key::Named(NamedKey::F1)
557 | Key::Named(NamedKey::F2)
558 | Key::Named(NamedKey::F3)
559 | Key::Named(NamedKey::Escape)
560 );
561
562 if !is_ui_control_key {
563 log::debug!("Blocking key while UI visible: {:?}", event.logical_key);
564 return;
565 }
566 }
567
568 if self.is_egui_using_keyboard() {
570 log::debug!("Blocking key event: egui wants keyboard input");
571 return;
572 }
573
574 let is_running = if let Some(terminal) = &self.terminal {
576 if let Ok(term) = terminal.try_lock() {
577 term.is_running()
578 } else {
579 true
580 }
581 } else {
582 true
583 };
584
585 if !is_running && event.state == ElementState::Pressed {
588 log::info!("Shell has exited, closing terminal on keypress");
589 if let Some(task) = self.refresh_task.take() {
591 task.abort();
592 log::info!("Refresh task aborted");
593 }
594 event_loop.exit();
595 return;
596 }
597
598 if event.state == ElementState::Pressed {
600 self.last_key_press = Some(std::time::Instant::now());
601 }
602
603 if self.handle_scroll_keys(&event) {
605 return; }
607
608 if self.handle_config_reload(&event) {
610 return; }
612
613 if self.handle_clipboard_history_keys(&event) {
615 return; }
617
618 if self.handle_fullscreen_toggle(&event) {
620 return; }
622
623 if self.handle_help_toggle(&event) {
625 return; }
627
628 if self.handle_settings_toggle(&event) {
630 return; }
632
633 if self.handle_shader_editor_toggle(&event) {
635 return; }
637
638 if self.handle_fps_overlay_toggle(&event) {
640 return; }
642
643 if self.handle_utility_shortcuts(&event, event_loop) {
645 return; }
647
648 if event.state == ElementState::Pressed && self.selection.is_some() {
650 self.selection = None;
651 if let Some(window) = &self.window {
652 window.request_redraw();
653 }
654 }
655
656 let is_tab = matches!(event.logical_key, Key::Named(NamedKey::Tab));
658 let is_space = matches!(event.logical_key, Key::Named(NamedKey::Space));
659 if is_tab {
660 log::debug!("Tab key event received, state={:?}", event.state);
661 }
662 if is_space {
663 log::debug!("Space key event received, state={:?}", event.state);
664 }
665
666 if let Some(bytes) = self.input_handler.handle_key_event(event)
668 && let Some(terminal) = &self.terminal
669 {
670 if is_tab {
671 log::debug!("Sending Tab key to terminal ({} bytes)", bytes.len());
672 }
673 if is_space {
674 log::debug!("Sending Space key to terminal ({} bytes)", bytes.len());
675 }
676 let terminal_clone = Arc::clone(terminal);
677
678 self.runtime.spawn(async move {
679 let term = terminal_clone.lock().await;
680 let _ = term.write(&bytes);
681 });
682 }
683 }
684
685 fn handle_scroll_keys(&mut self, event: &KeyEvent) -> bool {
686 use winit::event::ElementState;
687 use winit::keyboard::{Key, NamedKey};
688
689 if event.state != ElementState::Pressed {
690 return false;
691 }
692
693 let shift = self.input_handler.modifiers.state().shift_key();
694
695 let handled = match &event.logical_key {
696 Key::Named(NamedKey::PageUp) => {
697 self.scroll_up_page();
699 true
700 }
701 Key::Named(NamedKey::PageDown) => {
702 self.scroll_down_page();
704 true
705 }
706 Key::Named(NamedKey::Home) if shift => {
707 self.scroll_to_top();
709 true
710 }
711 Key::Named(NamedKey::End) if shift => {
712 self.scroll_to_bottom();
714 true
715 }
716 _ => false,
717 };
718
719 if handled && let Some(window) = &self.window {
720 window.request_redraw();
721 }
722
723 handled
724 }
725
726 fn handle_config_reload(&mut self, event: &KeyEvent) -> bool {
727 use winit::event::ElementState;
728 use winit::keyboard::{Key, NamedKey};
729
730 if event.state != ElementState::Pressed {
731 return false;
732 }
733
734 if matches!(event.logical_key, Key::Named(NamedKey::F5)) {
736 log::info!("Reloading configuration (F5 pressed)");
737 self.reload_config();
738 return true;
739 }
740
741 false
742 }
743
744 fn reload_config(&mut self) {
745 match Config::load() {
746 Ok(new_config) => {
747 log::info!("Configuration reloaded successfully");
748
749 self.config.auto_copy_selection = new_config.auto_copy_selection;
753
754 self.config.middle_click_paste = new_config.middle_click_paste;
756
757 if self.config.window_title != new_config.window_title {
759 self.config.window_title = new_config.window_title.clone();
760 if let Some(window) = &self.window {
761 window.set_title(&new_config.window_title);
762 }
763 }
764
765 if self.config.theme != new_config.theme {
767 self.config.theme = new_config.theme.clone();
768 if let Some(terminal) = &self.terminal
769 && let Ok(mut term) = terminal.try_lock()
770 {
771 term.set_theme(new_config.load_theme());
772 log::info!("Applied new theme: {}", new_config.theme);
773 }
774 }
775
776 if new_config.font_size != self.config.font_size {
781 log::info!(
782 "Font size changed from {} -> {} (applied live)",
783 self.config.font_size,
784 new_config.font_size
785 );
786 }
787
788 if new_config.cols != self.config.cols || new_config.rows != self.config.rows {
789 log::warn!("Terminal dimensions change requires restart");
790 }
791
792 if let Some(window) = &self.window {
794 window.request_redraw();
795 }
796 }
797 Err(e) => {
798 log::error!("Failed to reload configuration: {}", e);
799 }
800 }
801 }
802
803 fn handle_clipboard_history_keys(&mut self, event: &KeyEvent) -> bool {
804 use winit::event::ElementState;
805 use winit::keyboard::Key;
806
807 if self.clipboard_history_ui.visible {
809 if event.state == ElementState::Pressed {
810 match &event.logical_key {
811 Key::Named(winit::keyboard::NamedKey::Escape) => {
812 self.clipboard_history_ui.visible = false;
813 self.needs_redraw = true;
814 return true;
815 }
816 Key::Named(winit::keyboard::NamedKey::ArrowUp) => {
817 self.clipboard_history_ui.select_previous();
818 self.needs_redraw = true;
819 return true;
820 }
821 Key::Named(winit::keyboard::NamedKey::ArrowDown) => {
822 self.clipboard_history_ui.select_next();
823 self.needs_redraw = true;
824 return true;
825 }
826 Key::Named(winit::keyboard::NamedKey::Enter) => {
827 if let Some(entry) = self.clipboard_history_ui.selected_entry() {
829 let content = entry.content.clone();
830 self.clipboard_history_ui.visible = false;
831 self.paste_text(&content);
832 self.needs_redraw = true;
833 }
834 return true;
835 }
836 _ => {}
837 }
838 }
839 return true;
841 }
842
843 if event.state == ElementState::Pressed {
845 let ctrl = self.input_handler.modifiers.state().control_key();
846 let shift = self.input_handler.modifiers.state().shift_key();
847
848 if ctrl
849 && shift
850 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "h" || c.as_str() == "H")
851 {
852 self.toggle_clipboard_history();
853 return true;
854 }
855 }
856
857 false
858 }
859
860 fn toggle_clipboard_history(&mut self) {
861 if let Some(terminal) = &self.terminal
863 && let Ok(term) = terminal.try_lock()
864 {
865 let mut all_entries = Vec::new();
867 all_entries.extend(term.get_clipboard_history(ClipboardSlot::Primary));
868 all_entries.extend(term.get_clipboard_history(ClipboardSlot::Clipboard));
869 all_entries.extend(term.get_clipboard_history(ClipboardSlot::Selection));
870
871 all_entries.sort_by_key(|e| std::cmp::Reverse(e.timestamp));
873
874 self.clipboard_history_ui.update_entries(all_entries);
875 }
876
877 self.clipboard_history_ui.toggle();
878 self.needs_redraw = true;
879 log::debug!(
880 "Clipboard history UI toggled: {}",
881 self.clipboard_history_ui.visible
882 );
883 }
884
885 fn paste_text(&mut self, text: &str) {
886 if let Some(terminal) = &self.terminal {
887 let terminal_clone = Arc::clone(terminal);
888 let text = text.replace('\n', "\r");
890 self.runtime.spawn(async move {
891 let term = terminal_clone.lock().await;
892 let _ = term.write(text.as_bytes());
893 log::debug!("Pasted text from clipboard history ({} bytes)", text.len());
894 });
895 }
896 }
897
898 fn force_surface_reconfigure(&mut self) {
902 log::info!("Force surface reconfigure triggered");
903
904 if let Some(renderer) = &mut self.renderer {
905 renderer.reconfigure_surface();
907
908 renderer.clear_glyph_cache();
910
911 self.cached_cells = None;
913 }
914
915 #[cfg(target_os = "macos")]
917 {
918 if let Some(window) = &self.window
919 && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
920 {
921 log::warn!("Failed to reconfigure Metal layer: {}", e);
922 }
923 }
924
925 if let Some(window) = &self.window {
927 window.request_redraw();
928 }
929
930 self.needs_redraw = true;
931 }
932
933 fn handle_utility_shortcuts(
934 &mut self,
935 event: &KeyEvent,
936 _event_loop: &ActiveEventLoop,
937 ) -> bool {
938 use winit::event::ElementState;
939 use winit::keyboard::Key;
940
941 if event.state != ElementState::Pressed {
942 return false;
943 }
944
945 let ctrl = self.input_handler.modifiers.state().control_key();
946 let shift = self.input_handler.modifiers.state().shift_key();
947
948 if ctrl
950 && shift
951 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "k" || c.as_str() == "K")
952 {
953 let cleared = if let Some(terminal) = &self.terminal
955 && let Ok(term) = terminal.try_lock()
956 {
957 term.clear_scrollback();
958 true
959 } else {
960 false
961 };
962
963 if cleared {
964 self.cached_scrollback_len = 0;
965 self.set_scroll_target(0);
966 log::info!("Cleared scrollback buffer");
967 }
968 return true;
969 }
970
971 if ctrl
973 && !shift
974 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "l" || c.as_str() == "L")
975 {
976 if let Some(terminal) = &self.terminal {
977 let terminal_clone = Arc::clone(terminal);
978 let clear_sequence = vec![0x0C]; self.runtime.spawn(async move {
981 if let Ok(term) = terminal_clone.try_lock() {
982 let _ = term.write(&clear_sequence);
983 log::debug!("Sent clear screen sequence (Ctrl+L)");
984 }
985 });
986 }
987 return true;
988 }
989
990 if ctrl
992 && !shift
993 && (matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "+" || c.as_str() == "="))
994 {
995 self.config.font_size = (self.config.font_size + 1.0).min(72.0);
996 self.pending_font_rebuild = true;
997 log::info!(
998 "Font size increased to {} (applying live)",
999 self.config.font_size
1000 );
1001 if let Some(window) = &self.window {
1002 window.request_redraw();
1003 }
1004 return true;
1005 }
1006
1007 if ctrl
1009 && !shift
1010 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "-" || c.as_str() == "_")
1011 {
1012 self.config.font_size = (self.config.font_size - 1.0).max(6.0);
1013 self.pending_font_rebuild = true;
1014 log::info!(
1015 "Font size decreased to {} (applying live)",
1016 self.config.font_size
1017 );
1018 if let Some(window) = &self.window {
1019 window.request_redraw();
1020 }
1021 return true;
1022 }
1023
1024 if ctrl && !shift && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "0")
1026 {
1027 self.config.font_size = 14.0; self.pending_font_rebuild = true;
1029 log::info!("Font size reset to default (14.0, applying live)");
1030 if let Some(window) = &self.window {
1031 window.request_redraw();
1032 }
1033 return true;
1034 }
1035
1036 let super_key = self.input_handler.modifiers.state().super_key();
1038 let ctrl_or_cmd = ctrl || super_key;
1039
1040 if ctrl_or_cmd
1041 && !shift
1042 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == ",")
1043 {
1044 use crate::config::CursorStyle;
1045 use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
1046
1047 self.config.cursor_style = match self.config.cursor_style {
1049 CursorStyle::Block => CursorStyle::Beam,
1050 CursorStyle::Beam => CursorStyle::Underline,
1051 CursorStyle::Underline => CursorStyle::Block,
1052 };
1053
1054 log::info!("Cursor style changed to {:?}", self.config.cursor_style);
1055
1056 if let Some(terminal_mgr) = &self.terminal
1058 && let Ok(term_mgr) = terminal_mgr.try_lock()
1059 {
1060 let terminal = term_mgr.terminal();
1061 if let Some(mut term) = terminal.try_lock() {
1062 let term_style = match self.config.cursor_style {
1063 CursorStyle::Block => TermCursorStyle::SteadyBlock,
1064 CursorStyle::Beam => TermCursorStyle::SteadyBar,
1065 CursorStyle::Underline => TermCursorStyle::SteadyUnderline,
1066 };
1067 term.set_cursor_style(term_style);
1068 }
1069 }
1070
1071 self.cached_cells = None;
1073 self.last_cursor_pos = None;
1074 if let Some(window) = &self.window {
1075 window.request_redraw();
1076 }
1077 return true;
1078 }
1079
1080 if ctrl
1082 && shift
1083 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "s" || c.as_str() == "S")
1084 {
1085 self.take_screenshot();
1086 return true;
1087 }
1088
1089 if ctrl
1091 && shift
1092 && matches!(event.logical_key, Key::Character(ref c) if c.as_str() == "r" || c.as_str() == "R")
1093 {
1094 self.toggle_recording();
1095 return true;
1096 }
1097
1098 if ctrl && shift && matches!(event.logical_key, Key::Named(winit::keyboard::NamedKey::F5)) {
1100 log::info!("Manual surface reconfigure triggered via Ctrl+Shift+F5");
1101 self.force_surface_reconfigure();
1102 return true;
1103 }
1104
1105 false
1106 }
1107
1108 fn handle_fullscreen_toggle(&mut self, event: &KeyEvent) -> bool {
1109 use winit::event::ElementState;
1110 use winit::keyboard::{Key, NamedKey};
1111
1112 if event.state != ElementState::Pressed {
1113 return false;
1114 }
1115
1116 if matches!(event.logical_key, Key::Named(NamedKey::F11))
1118 && let Some(window) = &self.window
1119 {
1120 self.is_fullscreen = !self.is_fullscreen;
1121
1122 if self.is_fullscreen {
1123 window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
1124 log::info!("Entering fullscreen mode");
1125 } else {
1126 window.set_fullscreen(None);
1127 log::info!("Exiting fullscreen mode");
1128 }
1129
1130 return true;
1131 }
1132
1133 false
1134 }
1135
1136 fn handle_settings_toggle(&mut self, event: &KeyEvent) -> bool {
1137 use winit::event::ElementState;
1138 use winit::keyboard::{Key, NamedKey};
1139
1140 if event.state != ElementState::Pressed {
1141 return false;
1142 }
1143
1144 if matches!(event.logical_key, Key::Named(NamedKey::F12)) {
1146 self.settings_ui.toggle();
1147 log::info!(
1148 "Settings UI toggled: {}",
1149 if self.settings_ui.visible {
1150 "visible"
1151 } else {
1152 "hidden"
1153 }
1154 );
1155
1156 if let Some(window) = &self.window {
1158 window.request_redraw();
1159 }
1160
1161 return true;
1162 }
1163
1164 false
1165 }
1166
1167 fn handle_help_toggle(&mut self, event: &KeyEvent) -> bool {
1169 use winit::event::ElementState;
1170 use winit::keyboard::{Key, NamedKey};
1171
1172 if event.state != ElementState::Pressed {
1173 return false;
1174 }
1175
1176 if matches!(event.logical_key, Key::Named(NamedKey::F1)) {
1178 self.help_ui.toggle();
1179 log::info!(
1180 "Help UI toggled: {}",
1181 if self.help_ui.visible {
1182 "visible"
1183 } else {
1184 "hidden"
1185 }
1186 );
1187
1188 if let Some(window) = &self.window {
1190 window.request_redraw();
1191 }
1192
1193 return true;
1194 }
1195
1196 if matches!(event.logical_key, Key::Named(NamedKey::Escape)) && self.help_ui.visible {
1198 self.help_ui.visible = false;
1199 log::info!("Help UI closed via Escape");
1200
1201 if let Some(window) = &self.window {
1202 window.request_redraw();
1203 }
1204
1205 return true;
1206 }
1207
1208 false
1209 }
1210
1211 fn handle_shader_editor_toggle(&mut self, event: &KeyEvent) -> bool {
1213 use winit::event::ElementState;
1214 use winit::keyboard::{Key, NamedKey};
1215
1216 if event.state != ElementState::Pressed {
1217 return false;
1218 }
1219
1220 if matches!(event.logical_key, Key::Named(NamedKey::F11)) {
1222 if self.settings_ui.is_shader_editor_visible() {
1223 log::info!("Shader editor close requested via F11");
1225 } else {
1226 if self.settings_ui.open_shader_editor() {
1228 log::info!("Shader editor opened via F11");
1229 } else {
1230 log::warn!("Cannot open shader editor: no shader path configured in settings");
1231 }
1232 }
1233
1234 if let Some(window) = &self.window {
1236 window.request_redraw();
1237 }
1238
1239 return true;
1240 }
1241
1242 false
1243 }
1244
1245 fn handle_fps_overlay_toggle(&mut self, event: &KeyEvent) -> bool {
1247 use winit::event::ElementState;
1248 use winit::keyboard::{Key, NamedKey};
1249
1250 if event.state != ElementState::Pressed {
1251 return false;
1252 }
1253
1254 if matches!(event.logical_key, Key::Named(NamedKey::F3)) {
1256 self.show_fps_overlay = !self.show_fps_overlay;
1257 log::info!(
1258 "FPS overlay toggled: {}",
1259 if self.show_fps_overlay {
1260 "visible"
1261 } else {
1262 "hidden"
1263 }
1264 );
1265
1266 if let Some(window) = &self.window {
1268 window.request_redraw();
1269 }
1270
1271 return true;
1272 }
1273
1274 false
1275 }
1276
1277 fn scroll_up_page(&mut self) {
1278 if let Some(renderer) = &self.renderer {
1280 let char_height = self.config.font_size * 1.2;
1281 let page_size = (renderer.size().height as f32 / char_height) as usize;
1282
1283 let new_target = self.scroll_state.target_offset.saturating_add(page_size);
1284 let clamped_target = new_target.min(self.cached_scrollback_len);
1285 self.set_scroll_target(clamped_target);
1286 }
1287 }
1288
1289 fn scroll_down_page(&mut self) {
1290 if let Some(renderer) = &self.renderer {
1292 let char_height = self.config.font_size * 1.2;
1293 let page_size = (renderer.size().height as f32 / char_height) as usize;
1294
1295 let new_target = self.scroll_state.target_offset.saturating_sub(page_size);
1296 self.set_scroll_target(new_target);
1297 }
1298 }
1299
1300 fn scroll_to_top(&mut self) {
1301 self.set_scroll_target(self.cached_scrollback_len);
1302 }
1303
1304 fn scroll_to_bottom(&mut self) {
1305 self.set_scroll_target(0);
1306 }
1307
1308 fn is_egui_using_pointer(&self) -> bool {
1310 let any_ui_visible =
1312 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
1313 if !any_ui_visible {
1314 return false;
1315 }
1316
1317 if let Some(ctx) = &self.egui_ctx {
1319 ctx.is_using_pointer() || ctx.wants_pointer_input()
1320 } else {
1321 false
1322 }
1323 }
1324
1325 fn is_egui_using_keyboard(&self) -> bool {
1327 let any_ui_visible =
1329 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
1330 if !any_ui_visible {
1331 return false;
1332 }
1333
1334 if let Some(ctx) = &self.egui_ctx {
1336 ctx.wants_keyboard_input()
1337 } else {
1338 false
1339 }
1340 }
1341
1342 fn handle_mouse_wheel(&mut self, delta: MouseScrollDelta) {
1343 if let Some(terminal) = &self.terminal
1347 && let Ok(term) = terminal.try_lock()
1348 && term.is_mouse_tracking_enabled()
1349 {
1350 let scroll_lines = match delta {
1352 MouseScrollDelta::LineDelta(_x, y) => y as i32,
1353 MouseScrollDelta::PixelDelta(pos) => (pos.y / 20.0) as i32,
1354 };
1355
1356 if let Some((col, row)) =
1358 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1359 {
1360 let button = if scroll_lines > 0 { 64 } else { 65 };
1362 let count = scroll_lines.unsigned_abs().min(10);
1364
1365 let mut all_encoded = Vec::new();
1367 for _ in 0..count {
1368 let encoded = term.encode_mouse_event(button, col, row, true, 0);
1369 if !encoded.is_empty() {
1370 all_encoded.extend(encoded);
1371 }
1372 }
1373
1374 if !all_encoded.is_empty() {
1375 let terminal_clone = Arc::clone(terminal);
1376 let runtime = Arc::clone(&self.runtime);
1377 runtime.spawn(async move {
1378 let t = terminal_clone.lock().await;
1379 let _ = t.write(&all_encoded);
1380 });
1381 }
1382 }
1383 return; }
1385
1386 let scroll_lines = match delta {
1389 MouseScrollDelta::LineDelta(_x, y) => (y * self.config.mouse_scroll_speed) as i32,
1390 MouseScrollDelta::PixelDelta(pos) => (pos.y / 20.0) as i32,
1391 };
1392
1393 let scrollback_len = self.cached_scrollback_len;
1394
1395 let new_target = self.scroll_state.apply_scroll(scroll_lines, scrollback_len);
1397
1398 self.set_scroll_target(new_target);
1400 }
1401
1402 fn set_scroll_target(&mut self, new_offset: usize) {
1405 if self.scroll_state.set_target(new_offset) {
1406 if let Some(window) = &self.window {
1409 window.request_redraw();
1410 }
1411 }
1412 }
1413
1414 fn update_scroll_animation(&mut self) -> bool {
1417 self.scroll_state.update_animation()
1418 }
1419
1420 fn pixel_to_cell(&self, x: f64, y: f64) -> Option<(usize, usize)> {
1422 if let Some(renderer) = &self.renderer {
1423 let cell_width = renderer.cell_width() as f64;
1425 let cell_height = renderer.cell_height() as f64;
1426 let padding = renderer.window_padding() as f64;
1427
1428 let adjusted_x = (x - padding).max(0.0);
1430 let adjusted_y = (y - padding).max(0.0);
1431
1432 let col = (adjusted_x / cell_width) as usize;
1433 let row = (adjusted_y / cell_height) as usize;
1434
1435 Some((col, row))
1436 } else {
1437 None
1438 }
1439 }
1440
1441 fn should_show_scrollbar(&self) -> bool {
1443 if self.cached_scrollback_len == 0 {
1445 return false;
1446 }
1447
1448 if self.scroll_state.dragging {
1450 return true;
1451 }
1452
1453 if self.config.scrollbar_autohide_delay == 0 {
1455 return true;
1456 }
1457
1458 if self.scroll_state.offset > 0 || self.scroll_state.target_offset > 0 {
1460 return true;
1461 }
1462
1463 if let Some(window) = &self.window {
1465 let padding = 32.0; let width = window.inner_size().width as f64;
1467 let near_right = self.config.scrollbar_position != "left"
1468 && (width - self.mouse_position.0) <= padding;
1469 let near_left =
1470 self.config.scrollbar_position == "left" && self.mouse_position.0 <= padding;
1471 if near_left || near_right {
1472 return true;
1473 }
1474 }
1475
1476 self.scroll_state.last_activity.elapsed().as_millis()
1478 < self.config.scrollbar_autohide_delay as u128
1479 }
1480
1481 fn select_word_at(&mut self, col: usize, row: usize) {
1483 if let Some(terminal) = &self.terminal
1484 && let Ok(term) = terminal.try_lock()
1485 {
1486 let (cols, _rows) = term.dimensions();
1487 let visible_cells =
1488 term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1489 if visible_cells.is_empty() || cols == 0 {
1490 return;
1491 }
1492
1493 let cell_idx = row * cols + col;
1494 if cell_idx >= visible_cells.len() {
1495 return;
1496 }
1497
1498 let mut start_col = col;
1500 let mut end_col = col;
1501
1502 for c in (0..col).rev() {
1504 let idx = row * cols + c;
1505 if idx >= visible_cells.len() {
1506 break;
1507 }
1508 let ch = visible_cells[idx].grapheme.chars().next().unwrap_or('\0');
1509 if ch.is_alphanumeric() || ch == '_' {
1510 start_col = c;
1511 } else {
1512 break;
1513 }
1514 }
1515
1516 for c in col..cols {
1518 let idx = row * cols + c;
1519 if idx >= visible_cells.len() {
1520 break;
1521 }
1522 let ch = visible_cells[idx].grapheme.chars().next().unwrap_or('\0');
1523 if ch.is_alphanumeric() || ch == '_' {
1524 end_col = c;
1525 } else {
1526 break;
1527 }
1528 }
1529
1530 self.selection = Some(Selection::new(
1531 (start_col, row),
1532 (end_col, row),
1533 SelectionMode::Normal,
1534 ));
1535 }
1536 }
1537
1538 fn select_line_at(&mut self, row: usize) {
1540 if let Some(terminal) = &self.terminal
1541 && let Ok(term) = terminal.try_lock()
1542 {
1543 let (cols, _rows) = term.dimensions();
1544 if cols == 0 {
1545 return;
1546 }
1547
1548 self.selection = Some(Selection::new(
1550 (0, row),
1551 (cols.saturating_sub(1), row),
1552 SelectionMode::Line,
1553 ));
1554 }
1555 }
1556
1557 fn extend_line_selection(&mut self, current_row: usize) {
1559 if let Some(terminal) = &self.terminal
1560 && let Ok(term) = terminal.try_lock()
1561 {
1562 let (cols, _rows) = term.dimensions();
1563 if cols == 0 {
1564 return;
1565 }
1566
1567 let anchor_row = self.click_position.map(|(_, r)| r).unwrap_or(current_row);
1569
1570 if let Some(ref mut selection) = self.selection
1571 && selection.mode == SelectionMode::Line
1572 {
1573 if current_row >= anchor_row {
1576 selection.start = (0, anchor_row);
1578 selection.end = (cols.saturating_sub(1), current_row);
1579 } else {
1580 selection.start = (cols.saturating_sub(1), anchor_row);
1583 selection.end = (0, current_row);
1584 }
1585 }
1586 }
1587 }
1588
1589 fn get_selected_text(&self) -> Option<String> {
1591 if let (Some(selection), Some(terminal)) = (&self.selection, &self.terminal) {
1592 if let Ok(term) = terminal.try_lock() {
1593 let (start, end) = selection.normalized();
1594 let (start_col, start_row) = start;
1595 let (end_col, end_row) = end;
1596
1597 let (cols, rows) = term.dimensions();
1598 let visible_cells =
1599 term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1600 if visible_cells.is_empty() || cols == 0 {
1601 return None;
1602 }
1603
1604 let mut visible_lines = Vec::with_capacity(rows);
1605 for row in 0..rows {
1606 let start_idx = row * cols;
1607 let end_idx = start_idx.saturating_add(cols);
1608 if end_idx > visible_cells.len() {
1609 break;
1610 }
1611
1612 let mut line = String::with_capacity(cols);
1613 for cell in &visible_cells[start_idx..end_idx] {
1614 line.push_str(&cell.grapheme);
1615 }
1616 visible_lines.push(line);
1617 }
1618
1619 if visible_lines.is_empty() {
1620 return None;
1621 }
1622
1623 let mut selected_text = String::new();
1624 let max_row = visible_lines.len().saturating_sub(1);
1625 let start_row = start_row.min(max_row);
1626 let end_row = end_row.min(max_row);
1627
1628 if selection.mode == SelectionMode::Line {
1629 #[allow(clippy::needless_range_loop)]
1631 for row in start_row..=end_row {
1632 if row > start_row {
1633 selected_text.push('\n');
1634 }
1635 let line = &visible_lines[row];
1636 selected_text.push_str(line.trim_end());
1638 }
1639 } else if selection.mode == SelectionMode::Rectangular {
1640 let min_col = start_col.min(end_col);
1642 let max_col = start_col.max(end_col);
1643
1644 #[allow(clippy::needless_range_loop)]
1645 for row in start_row..=end_row {
1646 if row > start_row {
1647 selected_text.push('\n');
1648 }
1649 let line = &visible_lines[row];
1650 selected_text.push_str(&Self::extract_columns(
1651 line,
1652 min_col,
1653 Some(max_col),
1654 ));
1655 }
1656 } else if start_row == end_row {
1657 let line = &visible_lines[start_row];
1659 selected_text = Self::extract_columns(line, start_col, Some(end_col));
1660 } else {
1661 for (idx, row) in (start_row..=end_row).enumerate() {
1663 let line = &visible_lines[row];
1664 if idx == 0 {
1665 selected_text.push_str(&Self::extract_columns(line, start_col, None));
1666 } else if row == end_row {
1667 selected_text.push('\n');
1668 selected_text.push_str(&Self::extract_columns(line, 0, Some(end_col)));
1669 } else {
1670 selected_text.push('\n');
1671 selected_text.push_str(line);
1672 }
1673 }
1674 }
1675
1676 Some(selected_text)
1677 } else {
1678 None
1679 }
1680 } else {
1681 None
1682 }
1683 }
1684
1685 fn detect_urls(&mut self) {
1687 self.detected_urls.clear();
1688
1689 if let Some(terminal) = &self.terminal
1690 && let Ok(term) = terminal.try_lock()
1691 {
1692 let (cols, rows) = term.dimensions();
1693 let visible_cells =
1694 term.get_cells_with_scrollback(self.scroll_state.offset, None, false, None);
1695
1696 if visible_cells.is_empty() || cols == 0 {
1697 return;
1698 }
1699
1700 let mut hyperlink_urls = std::collections::HashMap::new();
1702 let all_hyperlinks = term.get_all_hyperlinks();
1703 for hyperlink_info in all_hyperlinks {
1704 if let Some((col, row)) = hyperlink_info.positions.first() {
1706 let cell_idx = row * cols + col;
1708 if let Some(cell) = visible_cells.get(cell_idx)
1709 && let Some(id) = cell.hyperlink_id
1710 {
1711 hyperlink_urls.insert(id, hyperlink_info.url.clone());
1712 }
1713 }
1714 }
1715
1716 for row in 0..rows {
1718 let start_idx = row * cols;
1719 let end_idx = start_idx.saturating_add(cols);
1720 if end_idx > visible_cells.len() {
1721 break;
1722 }
1723
1724 let row_cells = &visible_cells[start_idx..end_idx];
1725
1726 let mut line = String::with_capacity(cols);
1727 for cell in row_cells {
1728 line.push_str(&cell.grapheme);
1729 }
1730
1731 let absolute_row = row + self.scroll_state.offset;
1733
1734 let regex_urls = url_detection::detect_urls_in_line(&line, absolute_row);
1736 self.detected_urls.extend(regex_urls);
1737
1738 let osc8_urls =
1740 url_detection::detect_osc8_hyperlinks(row_cells, absolute_row, &hyperlink_urls);
1741 self.detected_urls.extend(osc8_urls);
1742 }
1743 }
1744 }
1745
1746 fn apply_url_underlines(
1749 &self,
1750 cells: &mut [crate::cell_renderer::Cell],
1751 renderer_size: &winit::dpi::PhysicalSize<u32>,
1752 ) {
1753 if self.detected_urls.is_empty() {
1754 return;
1755 }
1756
1757 let char_width = self.config.font_size * 0.6;
1759 let cols = (renderer_size.width as f32 / char_width) as usize;
1760
1761 let url_color = [79, 195, 247, 255];
1763
1764 for url in &self.detected_urls {
1766 if url.row < self.scroll_state.offset {
1768 continue; }
1770 let viewport_row = url.row - self.scroll_state.offset;
1771
1772 for col in url.start_col..url.end_col {
1774 let cell_idx = viewport_row * cols + col;
1775 if cell_idx < cells.len() {
1776 cells[cell_idx].fg_color = url_color;
1777 cells[cell_idx].underline = true; }
1779 }
1780 }
1781 }
1782
1783 fn try_send_mouse_event(&self, button: u8, pressed: bool) -> bool {
1788 if let Some(terminal) = &self.terminal
1789 && let Some((col, row)) =
1790 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1791 && let Ok(term) = terminal.try_lock()
1792 {
1793 let alt_screen_active = term.is_alt_screen_active();
1796
1797 if term.is_mouse_tracking_enabled() {
1799 let encoded = term.encode_mouse_event(button, col, row, pressed, 0);
1801
1802 if !encoded.is_empty() {
1803 let terminal_clone = Arc::clone(terminal);
1805 let runtime = Arc::clone(&self.runtime);
1806 runtime.spawn(async move {
1807 let t = terminal_clone.lock().await;
1808 let _ = t.write(&encoded);
1809 });
1810 }
1811 return true; }
1813
1814 if alt_screen_active {
1816 return true;
1817 }
1818 }
1819 false }
1821
1822 fn update_cursor_blink(&mut self) {
1824 if !self.config.cursor_blink {
1825 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1827 return;
1828 }
1829
1830 let now = std::time::Instant::now();
1831
1832 if let Some(last_key) = self.last_key_press
1834 && now.duration_since(last_key).as_millis() < 500
1835 {
1836 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1837 self.last_cursor_blink = Some(now);
1838 return;
1839 }
1840
1841 let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
1843
1844 if let Some(last_blink) = self.last_cursor_blink {
1845 let elapsed = now.duration_since(last_blink);
1846 let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
1847
1848 self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
1850 .abs()
1851 .clamp(0.0, 1.0);
1852
1853 if elapsed >= blink_interval * 2 {
1855 self.last_cursor_blink = Some(now);
1856 }
1857 } else {
1858 self.cursor_opacity = 1.0;
1860 self.last_cursor_blink = Some(now);
1861 }
1862 }
1863
1864 fn handle_mouse_button(&mut self, button: MouseButton, state: ElementState) {
1865 self.mouse_button_pressed = state == ElementState::Pressed;
1867
1868 if button == MouseButton::Left
1871 && let Some(ref mut renderer) = self.renderer
1872 {
1873 renderer.set_shader_mouse_button(
1874 state == ElementState::Pressed,
1875 self.mouse_position.0 as f32,
1876 self.mouse_position.1 as f32,
1877 );
1878 }
1879
1880 match button {
1881 MouseButton::Left => {
1882 if state == ElementState::Pressed
1885 && self.input_handler.modifiers.state().control_key()
1886 && let Some((col, row)) =
1887 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1888 {
1889 let adjusted_row = row + self.scroll_state.offset;
1891
1892 if let Some(url) =
1893 url_detection::find_url_at_position(&self.detected_urls, col, adjusted_row)
1894 {
1895 if let Err(e) = url_detection::open_url(&url.url) {
1896 log::error!("Failed to open URL: {}", e);
1897 }
1898 return; }
1900 }
1901
1902 if self.try_send_mouse_event(0, state == ElementState::Pressed) {
1905 return; }
1907
1908 if state == ElementState::Pressed {
1909 let mouse_x = self.mouse_position.0 as f32;
1912 let mouse_y = self.mouse_position.1 as f32;
1913
1914 if let Some(renderer) = &self.renderer
1915 && renderer.scrollbar_track_contains_x(mouse_x)
1916 {
1917 self.scroll_state.dragging = true;
1918 self.scroll_state.last_activity = std::time::Instant::now();
1919
1920 let thumb_bounds = renderer.scrollbar_thumb_bounds();
1921 if renderer.scrollbar_contains_point(mouse_x, mouse_y) {
1922 self.scroll_state.drag_offset = thumb_bounds
1924 .map(|(thumb_top, thumb_height)| {
1925 (mouse_y - thumb_top).clamp(0.0, thumb_height)
1926 })
1927 .unwrap_or(0.0);
1928 } else {
1929 self.scroll_state.drag_offset = thumb_bounds
1931 .map(|(_, thumb_height)| thumb_height / 2.0)
1932 .unwrap_or(0.0);
1933 }
1934
1935 self.drag_scrollbar_to(mouse_y);
1936 return; }
1938
1939 if let Some((col, row)) =
1942 self.pixel_to_cell(self.mouse_position.0, self.mouse_position.1)
1943 {
1944 let now = std::time::Instant::now();
1945 let same_position = self.click_position == Some((col, row));
1946
1947 let threshold_ms = if self.click_count == 1 {
1949 self.config.mouse_double_click_threshold
1950 } else {
1951 self.config.mouse_triple_click_threshold
1952 };
1953 let click_threshold = std::time::Duration::from_millis(threshold_ms);
1954
1955 if same_position
1957 && let Some(last_time) = self.last_click_time
1958 && now.duration_since(last_time) < click_threshold
1959 {
1960 self.click_count = (self.click_count + 1).min(3);
1961 } else {
1962 self.click_count = 1;
1963 self.selection = None;
1965 }
1966
1967 self.last_click_time = Some(now);
1968 self.click_position = Some((col, row));
1969
1970 if self.click_count == 2 {
1972 self.select_word_at(col, row);
1974 self.is_selecting = false; if let Some(window) = &self.window {
1976 window.request_redraw();
1977 }
1978 } else if self.click_count == 3 {
1979 self.select_line_at(row);
1981 self.is_selecting = true; if let Some(window) = &self.window {
1983 window.request_redraw();
1984 }
1985 } else {
1986 self.is_selecting = false;
1988 self.selection = None;
1989 if let Some(window) = &self.window {
1990 window.request_redraw();
1991 }
1992 }
1993 }
1994 } else {
1995 if self.scroll_state.dragging {
1997 self.scroll_state.dragging = false;
1998 self.scroll_state.drag_offset = 0.0;
1999 return;
2000 }
2001
2002 self.is_selecting = false;
2004
2005 if let Some(mut selected_text) = self.get_selected_text()
2006 && !selected_text.is_empty()
2007 {
2008 if !self.config.copy_trailing_newline {
2010 while selected_text.ends_with('\n') || selected_text.ends_with('\r') {
2011 selected_text.pop();
2012 }
2013 }
2014
2015 if let Err(e) = self.input_handler.copy_to_primary_selection(&selected_text)
2017 {
2018 log::debug!("Failed to copy to primary selection: {}", e);
2019 } else {
2020 log::debug!(
2021 "Copied {} chars to primary selection",
2022 selected_text.len()
2023 );
2024 }
2025
2026 if self.config.auto_copy_selection {
2028 if let Err(e) = self.input_handler.copy_to_clipboard(&selected_text) {
2029 log::error!("Failed to copy to clipboard: {}", e);
2030 } else {
2031 log::debug!("Copied {} chars to clipboard", selected_text.len());
2032 }
2033 }
2034
2035 if let Some(terminal) = &self.terminal
2037 && let Ok(term) = terminal.try_lock()
2038 {
2039 term.add_to_clipboard_history(
2040 ClipboardSlot::Clipboard,
2041 selected_text.clone(),
2042 None,
2043 );
2044 }
2045 }
2046 }
2047 }
2048 MouseButton::Middle => {
2049 if self.try_send_mouse_event(1, state == ElementState::Pressed) {
2051 return; }
2053
2054 if state == ElementState::Pressed && self.config.middle_click_paste {
2056 if let Some(bytes) = self.input_handler.paste_from_primary_selection()
2058 && let Some(terminal) = &self.terminal
2059 {
2060 let terminal_clone = Arc::clone(terminal);
2061 self.runtime.spawn(async move {
2062 let term = terminal_clone.lock().await;
2063 let _ = term.write(&bytes);
2064 });
2065 }
2066 }
2067 }
2068 MouseButton::Right => {
2069 let _ = self.try_send_mouse_event(2, state == ElementState::Pressed);
2071 }
2073 _ => {}
2074 }
2075 }
2076
2077 fn handle_mouse_move(&mut self, position: (f64, f64)) {
2078 self.mouse_position = position;
2079
2080 if let Some(ref mut renderer) = self.renderer {
2083 renderer.set_shader_mouse_position(position.0 as f32, position.1 as f32);
2084 }
2085
2086 if let Some((col, row)) = self.pixel_to_cell(position.0, position.1) {
2089 let adjusted_row = row + self.scroll_state.offset;
2090 let url_opt =
2091 url_detection::find_url_at_position(&self.detected_urls, col, adjusted_row);
2092
2093 if let Some(url) = url_opt {
2094 if self.hovered_url.as_ref() != Some(&url.url) {
2096 self.hovered_url = Some(url.url.clone());
2097 if let Some(window) = &self.window {
2098 window.set_cursor(winit::window::CursorIcon::Pointer);
2100 let tooltip_title = format!("{} - {}", self.config.window_title, url.url);
2101 window.set_title(&tooltip_title);
2102 }
2103 }
2104 } else {
2105 if self.hovered_url.is_some() {
2107 self.hovered_url = None;
2108 if let Some(window) = &self.window {
2109 window.set_cursor(winit::window::CursorIcon::Text);
2110 if self.config.allow_title_change && !self.cached_terminal_title.is_empty()
2112 {
2113 window.set_title(&self.cached_terminal_title);
2114 } else {
2115 window.set_title(&self.config.window_title);
2116 }
2117 }
2118 }
2119 }
2120 }
2121
2122 if let Some(terminal) = &self.terminal
2125 && let Some((col, row)) = self.pixel_to_cell(position.0, position.1)
2126 && let Ok(term) = terminal.try_lock()
2127 && term.should_report_mouse_motion(self.mouse_button_pressed)
2128 {
2129 let button = if self.mouse_button_pressed {
2131 32 } else {
2133 35 };
2135
2136 let encoded = term.encode_mouse_event(button, col, row, true, 0);
2137 if !encoded.is_empty() {
2138 let terminal_clone = Arc::clone(terminal);
2139 let runtime = Arc::clone(&self.runtime);
2140 runtime.spawn(async move {
2141 let t = terminal_clone.lock().await;
2142 let _ = t.write(&encoded);
2143 });
2144 }
2145 return; }
2147
2148 if self.scroll_state.dragging {
2150 self.scroll_state.last_activity = std::time::Instant::now();
2151 self.drag_scrollbar_to(position.1 as f32);
2152 return; }
2154
2155 let alt_screen_active = self
2158 .terminal
2159 .as_ref()
2160 .and_then(|t| t.try_lock().ok())
2161 .is_some_and(|term| term.is_alt_screen_active());
2162
2163 if let Some((col, row)) = self.pixel_to_cell(position.0, position.1)
2164 && self.mouse_button_pressed
2165 && !alt_screen_active
2166 {
2167 if self.click_count == 1 && !self.is_selecting {
2168 if let Some(click_pos) = self.click_position
2170 && click_pos != (col, row)
2171 {
2172 self.is_selecting = true;
2173 let mode = if self.input_handler.modifiers.state().alt_key() {
2175 SelectionMode::Rectangular
2176 } else {
2177 SelectionMode::Normal
2178 };
2179 self.selection = Some(Selection::new(
2180 self.click_position.unwrap(),
2181 (col, row),
2182 mode,
2183 ));
2184 if let Some(window) = &self.window {
2185 window.request_redraw();
2186 }
2187 }
2188 } else if self.is_selecting {
2189 if let Some(ref selection) = self.selection {
2191 if selection.mode == SelectionMode::Line {
2192 self.extend_line_selection(row);
2194 if let Some(window) = &self.window {
2195 window.request_redraw();
2196 }
2197 } else {
2198 if let Some(ref mut sel) = self.selection {
2200 sel.end = (col, row);
2201 if let Some(window) = &self.window {
2202 window.request_redraw();
2203 }
2204 }
2205 }
2206 }
2207 }
2208 }
2209 }
2210
2211 fn drag_scrollbar_to(&mut self, mouse_y: f32) {
2212 if let Some(renderer) = &self.renderer {
2213 let adjusted_y = mouse_y - self.scroll_state.drag_offset;
2214 if let Some(new_offset) = renderer.scrollbar_mouse_y_to_scroll_offset(adjusted_y)
2215 && self.scroll_state.offset != new_offset
2216 {
2217 self.scroll_state.offset = new_offset;
2219 self.scroll_state.target_offset = new_offset;
2220 self.scroll_state.animated_offset = new_offset as f64;
2221 self.scroll_state.animation_start = None;
2222
2223 if let Some(window) = &self.window {
2224 window.request_redraw();
2225 }
2226 }
2227 }
2228 }
2229
2230 fn render(&mut self) {
2231 if self.is_shutting_down {
2233 return;
2234 }
2235
2236 let absolute_start = std::time::Instant::now();
2237
2238 self.needs_redraw = false;
2241
2242 let frame_start = std::time::Instant::now();
2244
2245 if let Some(last_start) = self.debug_last_frame_start {
2247 let frame_time = frame_start.duration_since(last_start);
2248 self.debug_frame_times.push(frame_time);
2249 if self.debug_frame_times.len() > 60 {
2250 self.debug_frame_times.remove(0);
2251 }
2252 }
2253 self.debug_last_frame_start = Some(frame_start);
2254
2255 let animation_running = self.update_scroll_animation();
2257
2258 if self.pending_font_rebuild {
2260 if let Err(e) = self.rebuild_renderer() {
2261 log::error!("Failed to rebuild renderer after font change: {}", e);
2262 }
2263 self.pending_font_rebuild = false;
2264 }
2265
2266 let (renderer_size, visible_lines) = if let Some(renderer) = &self.renderer {
2267 (renderer.size(), renderer.grid_size().1)
2268 } else {
2269 return;
2270 };
2271
2272 let terminal = if let Some(terminal) = &self.terminal {
2273 terminal
2274 } else {
2275 return;
2276 };
2277
2278 let _is_running = if let Ok(term) = terminal.try_lock() {
2280 term.is_running()
2281 } else {
2282 true };
2284
2285 if animation_running && let Some(window) = &self.window {
2287 window.request_redraw();
2288 }
2289
2290 let (cells, current_cursor_pos, cursor_style) = if let Ok(term) = terminal.try_lock() {
2292 let current_generation = term.update_generation();
2294
2295 let (selection, rectangular) = if let Some(sel) = self.selection {
2297 (
2298 Some(sel.normalized()),
2299 sel.mode == SelectionMode::Rectangular,
2300 )
2301 } else {
2302 (None, false)
2303 };
2304
2305 let current_cursor_pos = if self.scroll_state.offset == 0 && term.is_cursor_visible() {
2308 Some(term.cursor_position())
2309 } else {
2310 None
2311 };
2312
2313 let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
2314
2315 let cursor_style = if current_cursor_pos.is_some() {
2317 Some(term.cursor_style())
2318 } else {
2319 None
2320 };
2321
2322 log::trace!(
2323 "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
2324 current_cursor_pos,
2325 self.cursor_opacity,
2326 cursor_style,
2327 self.scroll_state.offset,
2328 term.is_cursor_visible()
2329 );
2330
2331 let needs_regeneration = self.cached_cells.is_none()
2334 || current_generation != self.last_generation
2335 || self.scroll_state.offset != self.last_scroll_offset
2336 || current_cursor_pos != self.last_cursor_pos || self.selection != self.last_selection; let cell_gen_start = std::time::Instant::now();
2340 let (cells, is_cache_hit) = if needs_regeneration {
2341 let fresh_cells = term.get_cells_with_scrollback(
2343 self.scroll_state.offset,
2344 selection,
2345 rectangular,
2346 cursor,
2347 );
2348
2349 self.cached_cells = Some(fresh_cells.clone());
2351 self.last_generation = current_generation;
2352 self.last_scroll_offset = self.scroll_state.offset;
2353 self.last_cursor_pos = current_cursor_pos;
2354 self.last_selection = self.selection;
2355
2356 (fresh_cells, false)
2357 } else {
2358 (self.cached_cells.as_ref().unwrap().clone(), true)
2361 };
2362 self.debug_cache_hit = is_cache_hit;
2363 self.debug_cell_gen_time = cell_gen_start.elapsed();
2364
2365 (cells, current_cursor_pos, cursor_style)
2366 } else {
2367 return; };
2369
2370 let (scrollback_len, terminal_title) = if let Ok(term) = terminal.try_lock() {
2375 (term.scrollback_len(), term.get_title())
2376 } else {
2377 (
2378 self.cached_scrollback_len,
2379 self.cached_terminal_title.clone(),
2380 )
2381 };
2382
2383 self.cached_scrollback_len = scrollback_len;
2384 self.scroll_state
2385 .clamp_to_scrollback(self.cached_scrollback_len);
2386
2387 if self.config.allow_title_change
2390 && self.hovered_url.is_none()
2391 && terminal_title != self.cached_terminal_title
2392 && let Some(window) = &self.window
2393 {
2394 self.cached_terminal_title = terminal_title.clone();
2395 if terminal_title.is_empty() {
2396 window.set_title(&self.config.window_title);
2398 } else {
2399 window.set_title(&terminal_title);
2401 }
2402 }
2403
2404 let total_lines = visible_lines + scrollback_len;
2406
2407 let url_detect_start = std::time::Instant::now();
2410 let debug_url_detect_time = if !self.debug_cache_hit {
2411 self.detect_urls();
2413 url_detect_start.elapsed()
2414 } else {
2415 std::time::Duration::ZERO
2417 };
2418
2419 let url_underline_start = std::time::Instant::now();
2421 let mut cells = cells; self.apply_url_underlines(&mut cells, &renderer_size);
2423 let _debug_url_underline_time = url_underline_start.elapsed();
2424
2425 self.update_cursor_blink();
2427
2428 let render_start = std::time::Instant::now();
2429
2430 let mut debug_update_cells_time = std::time::Duration::ZERO;
2431 #[allow(unused_assignments)]
2432 let mut debug_graphics_time = std::time::Duration::ZERO;
2433 #[allow(unused_assignments)]
2434 let mut debug_actual_render_time = std::time::Duration::ZERO;
2435 let _ = &debug_actual_render_time;
2436 let mut pending_clipboard_action = ClipboardHistoryAction::None;
2438
2439 let show_scrollbar = self.should_show_scrollbar();
2440
2441 if let Some(renderer) = &mut self.renderer {
2442 if !self.debug_cache_hit {
2445 let t = std::time::Instant::now();
2446 renderer.update_cells(&cells);
2447 debug_update_cells_time = t.elapsed();
2448 }
2449
2450 if let (Some(pos), Some(opacity), Some(style)) =
2452 (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
2453 {
2454 renderer.update_cursor(pos, opacity, style);
2455 let cursor_color = [
2458 self.config.cursor_color[0] as f32 / 255.0,
2459 self.config.cursor_color[1] as f32 / 255.0,
2460 self.config.cursor_color[2] as f32 / 255.0,
2461 1.0,
2462 ];
2463 renderer.update_shader_cursor(pos.0, pos.1, opacity, cursor_color, style);
2464 } else {
2465 renderer.clear_cursor();
2466 }
2467
2468 if self.settings_ui.visible {
2470 let ui_cfg = self.settings_ui.current_config().clone();
2471 if (ui_cfg.window_opacity - self.config.window_opacity).abs() > 1e-4 {
2472 log::info!(
2473 "Syncing live opacity from UI {:.3} (app {:.3})",
2474 ui_cfg.window_opacity,
2475 self.config.window_opacity
2476 );
2477 self.config.window_opacity = ui_cfg.window_opacity;
2478 }
2479
2480 renderer.update_opacity(self.config.window_opacity);
2481 self.last_applied_opacity = self.config.window_opacity;
2482 self.cached_cells = None;
2483 if let Some(window) = &self.window {
2484 window.request_redraw();
2485 }
2486 }
2487
2488 renderer.update_scrollbar(self.scroll_state.offset, visible_lines, total_lines);
2490
2491 let anim_start = std::time::Instant::now();
2493 if let Some(terminal) = &self.terminal {
2494 let terminal = terminal.blocking_lock();
2495 if terminal.update_animations() {
2496 if let Some(window) = &self.window {
2498 window.request_redraw();
2499 }
2500 }
2501 }
2502 let debug_anim_time = anim_start.elapsed();
2503
2504 let graphics_start = std::time::Instant::now();
2508 if let Some(terminal) = &self.terminal {
2509 let terminal = terminal.blocking_lock();
2510 let mut graphics = terminal.get_graphics_with_animations();
2511 let scrollback_len = terminal.scrollback_len();
2512
2513 let scrollback_graphics = terminal.get_scrollback_graphics();
2515 let scrollback_count = scrollback_graphics.len();
2516 graphics.extend(scrollback_graphics);
2517
2518 debug_info!(
2519 "APP",
2520 "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
2521 graphics.len(),
2522 scrollback_count,
2523 self.scroll_state.offset,
2524 scrollback_len
2525 );
2526 if let Err(e) = renderer.update_graphics(
2527 &graphics,
2528 self.scroll_state.offset,
2529 scrollback_len,
2530 visible_lines,
2531 ) {
2532 log::error!("Failed to update graphics: {}", e);
2533 }
2534 }
2535 debug_graphics_time = graphics_start.elapsed();
2536
2537 let visual_bell_intensity = if let Some(flash_start) = self.visual_bell_flash {
2539 const FLASH_DURATION_MS: u128 = 150;
2540 let elapsed = flash_start.elapsed().as_millis();
2541 if elapsed < FLASH_DURATION_MS {
2542 if let Some(window) = &self.window {
2544 window.request_redraw();
2545 }
2546 0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
2548 } else {
2549 self.visual_bell_flash = None;
2551 0.0
2552 }
2553 } else {
2554 0.0
2555 };
2556
2557 renderer.set_visual_bell_intensity(visual_bell_intensity);
2559
2560 let egui_start = std::time::Instant::now();
2562
2563 let show_fps = self.show_fps_overlay;
2565 let fps_value = self.fps_value;
2566 let frame_time_ms = if !self.debug_frame_times.is_empty() {
2567 let avg = self.debug_frame_times.iter().sum::<std::time::Duration>()
2568 / self.debug_frame_times.len() as u32;
2569 avg.as_secs_f64() * 1000.0
2570 } else {
2571 0.0
2572 };
2573
2574 #[allow(clippy::type_complexity)]
2576 let mut pending_config_update: Option<(
2577 Option<crate::config::Config>,
2578 Option<crate::config::Config>,
2579 Option<ShaderEditorResult>,
2580 Option<CursorShaderEditorResult>,
2581 )> = None;
2582
2583 let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
2584 (&self.egui_ctx, &mut self.egui_state)
2585 {
2586 let raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
2587
2588 let egui_output = egui_ctx.run(raw_input, |ctx| {
2589 if show_fps {
2591 egui::Area::new(egui::Id::new("fps_overlay"))
2592 .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
2593 .order(egui::Order::Foreground)
2594 .show(ctx, |ui| {
2595 egui::Frame::NONE
2596 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
2597 .inner_margin(egui::Margin::same(8))
2598 .corner_radius(4.0)
2599 .show(ui, |ui| {
2600 ui.style_mut().visuals.override_text_color =
2601 Some(egui::Color32::from_rgb(0, 255, 0));
2602 ui.label(
2603 egui::RichText::new(format!(
2604 "FPS: {:.1}\nFrame: {:.2}ms",
2605 fps_value, frame_time_ms
2606 ))
2607 .monospace()
2608 .size(14.0),
2609 );
2610 });
2611 });
2612 }
2613
2614 let settings_result = self.settings_ui.show(ctx);
2616 pending_config_update = Some(settings_result);
2617
2618 self.help_ui.show(ctx);
2620
2621 pending_clipboard_action = self.clipboard_history_ui.show(ctx);
2623 });
2624
2625 egui_state.handle_platform_output(
2628 self.window.as_ref().unwrap(),
2629 egui_output.platform_output.clone(),
2630 );
2631
2632 Some((egui_output, egui_ctx))
2633 } else {
2634 None
2635 };
2636
2637 if let Some((
2639 config_to_save,
2640 config_for_live_update,
2641 shader_apply,
2642 cursor_shader_apply,
2643 )) = pending_config_update
2644 {
2645 if let Some(shader_result) = shader_apply {
2647 log::info!(
2648 "Applying background shader from editor ({} bytes)",
2649 shader_result.source.len()
2650 );
2651 match renderer.reload_shader_from_source(&shader_result.source) {
2652 Ok(()) => {
2653 log::info!("Background shader applied successfully from editor");
2654 self.settings_ui.clear_shader_error();
2655 }
2656 Err(e) => {
2657 let error_msg = format!("{:#}", e);
2658 log::error!("Background shader compilation failed: {}", error_msg);
2659 self.settings_ui.set_shader_error(Some(error_msg));
2660 }
2661 }
2662 }
2663
2664 if let Some(cursor_shader_result) = cursor_shader_apply {
2666 log::info!(
2667 "Applying cursor shader from editor ({} bytes)",
2668 cursor_shader_result.source.len()
2669 );
2670 match renderer.reload_cursor_shader_from_source(&cursor_shader_result.source) {
2671 Ok(()) => {
2672 log::info!("Cursor shader applied successfully from editor");
2673 self.settings_ui.clear_cursor_shader_error();
2674 }
2675 Err(e) => {
2676 let error_msg = format!("{:#}", e);
2677 log::error!("Cursor shader compilation failed: {}", error_msg);
2678 self.settings_ui.set_cursor_shader_error(Some(error_msg));
2679 }
2680 }
2681 }
2682 if let Some(live_config) = config_for_live_update {
2684 let theme_changed = live_config.theme != self.config.theme;
2685 let shader_animation_changed =
2686 live_config.custom_shader_animation != self.config.custom_shader_animation;
2687 let shader_enabled_changed =
2688 live_config.custom_shader_enabled != self.config.custom_shader_enabled;
2689 let shader_path_changed =
2690 live_config.custom_shader != self.config.custom_shader;
2691 let shader_speed_changed = (live_config.custom_shader_animation_speed
2692 - self.config.custom_shader_animation_speed)
2693 .abs()
2694 > f32::EPSILON;
2695 let shader_full_content_changed = live_config.custom_shader_full_content
2696 != self.config.custom_shader_full_content;
2697 let shader_text_opacity_changed = (live_config.custom_shader_text_opacity
2698 - self.config.custom_shader_text_opacity)
2699 .abs()
2700 > f32::EPSILON;
2701 let cursor_shader_config_changed = live_config.cursor_shader_color
2702 != self.config.cursor_shader_color
2703 || (live_config.cursor_shader_trail_duration
2704 - self.config.cursor_shader_trail_duration)
2705 .abs()
2706 > f32::EPSILON
2707 || (live_config.cursor_shader_glow_radius
2708 - self.config.cursor_shader_glow_radius)
2709 .abs()
2710 > f32::EPSILON
2711 || (live_config.cursor_shader_glow_intensity
2712 - self.config.cursor_shader_glow_intensity)
2713 .abs()
2714 > f32::EPSILON;
2715 let cursor_shader_path_changed =
2716 live_config.cursor_shader != self.config.cursor_shader;
2717 let cursor_shader_enabled_changed =
2718 live_config.cursor_shader_enabled != self.config.cursor_shader_enabled;
2719 let cursor_shader_animation_changed =
2720 live_config.cursor_shader_animation != self.config.cursor_shader_animation;
2721 let cursor_shader_speed_changed = (live_config.cursor_shader_animation_speed
2722 - self.config.cursor_shader_animation_speed)
2723 .abs()
2724 > f32::EPSILON;
2725 let _scrollbar_position_changed =
2726 live_config.scrollbar_position != self.config.scrollbar_position;
2727 let window_title_changed = live_config.window_title != self.config.window_title;
2728 let window_decorations_changed =
2729 live_config.window_decorations != self.config.window_decorations;
2730 let max_fps_changed = live_config.max_fps != self.config.max_fps;
2731 let cursor_style_changed = live_config.cursor_style != self.config.cursor_style;
2732 let cursor_color_changed =
2733 live_config.cursor_color != self.config.cursor_color;
2734 let bg_enabled_changed = live_config.background_image_enabled
2735 != self.config.background_image_enabled;
2736 let bg_path_changed =
2737 live_config.background_image != self.config.background_image;
2738 let bg_mode_changed =
2739 live_config.background_image_mode != self.config.background_image_mode;
2740 let bg_opacity_changed = (live_config.background_image_opacity
2741 - self.config.background_image_opacity)
2742 .abs()
2743 > f32::EPSILON;
2744 let font_changed = live_config.font_family != self.config.font_family
2745 || live_config.font_family_bold != self.config.font_family_bold
2746 || live_config.font_family_italic != self.config.font_family_italic
2747 || live_config.font_family_bold_italic
2748 != self.config.font_family_bold_italic
2749 || (live_config.font_size - self.config.font_size).abs() > f32::EPSILON
2750 || (live_config.line_spacing - self.config.line_spacing).abs()
2751 > f32::EPSILON
2752 || (live_config.char_spacing - self.config.char_spacing).abs()
2753 > f32::EPSILON;
2754 let padding_changed = (live_config.window_padding - self.config.window_padding)
2755 .abs()
2756 > f32::EPSILON;
2757 log::info!(
2758 "Applying live config update - opacity: {}{}{}",
2759 live_config.window_opacity,
2760 if theme_changed {
2761 " (theme changed)"
2762 } else {
2763 ""
2764 },
2765 if font_changed { " (font changed)" } else { "" }
2766 );
2767 self.config = live_config;
2768 self.scroll_state.last_activity = std::time::Instant::now();
2769
2770 if let Some(window) = &self.window {
2772 window.set_window_level(if self.config.window_always_on_top {
2774 winit::window::WindowLevel::AlwaysOnTop
2775 } else {
2776 winit::window::WindowLevel::Normal
2777 });
2778
2779 if window_title_changed {
2781 window.set_title(&self.config.window_title);
2782 log::info!("Updated window title to: {}", self.config.window_title);
2783 }
2784
2785 if window_decorations_changed {
2787 window.set_decorations(self.config.window_decorations);
2788 log::info!(
2789 "Updated window decorations: {}",
2790 self.config.window_decorations
2791 );
2792 }
2793
2794 window.request_redraw();
2796 }
2797
2798 if max_fps_changed {
2800 if let Some(old_task) = self.refresh_task.take() {
2802 old_task.abort();
2803 }
2804 if let Some(window) = &self.window {
2806 let window_clone = Arc::clone(window);
2807 let refresh_interval_ms = 1000 / self.config.max_fps.max(1); let handle = self.runtime.spawn(async move {
2809 let mut interval = tokio::time::interval(
2810 tokio::time::Duration::from_millis(refresh_interval_ms as u64),
2811 );
2812 loop {
2813 interval.tick().await;
2814 window_clone.request_redraw();
2815 }
2816 });
2817 self.refresh_task = Some(handle);
2818 log::info!(
2819 "Updated max_fps to {} ({}ms interval)",
2820 self.config.max_fps,
2821 refresh_interval_ms
2822 );
2823 }
2824 }
2825
2826 renderer.update_opacity(self.config.window_opacity);
2828 renderer.update_scrollbar_appearance(
2829 self.config.scrollbar_width,
2830 self.config.scrollbar_thumb_color,
2831 self.config.scrollbar_track_color,
2832 );
2833 if cursor_style_changed {
2836 if let Some(terminal_mgr) = &self.terminal
2839 && let Ok(term_mgr) = terminal_mgr.try_lock()
2840 {
2841 let terminal = term_mgr.terminal();
2843 if let Some(mut term) = terminal.try_lock() {
2844 use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
2846 let term_style = match self.config.cursor_style {
2847 crate::config::CursorStyle::Block => {
2848 TermCursorStyle::SteadyBlock
2849 }
2850 crate::config::CursorStyle::Underline => {
2851 TermCursorStyle::SteadyUnderline
2852 }
2853 crate::config::CursorStyle::Beam => TermCursorStyle::SteadyBar,
2854 };
2855 term.set_cursor_style(term_style);
2856 }
2857 }
2858
2859 self.cached_cells = None;
2861 self.last_cursor_pos = None;
2862 if let Some(window) = &self.window {
2863 window.request_redraw();
2864 }
2865 }
2866
2867 if cursor_color_changed {
2869 renderer.update_cursor_color(self.config.cursor_color);
2870 self.cached_cells = None;
2872 self.last_cursor_pos = None;
2873 if let Some(window) = &self.window {
2874 window.request_redraw();
2875 }
2876 }
2877
2878 if self.config.background_image_enabled {
2879 renderer
2880 .update_background_image_opacity(self.config.background_image_opacity);
2881 }
2882
2883 if bg_enabled_changed
2884 || bg_path_changed
2885 || bg_mode_changed
2886 || bg_opacity_changed
2887 {
2888 renderer.set_background_image_enabled(
2889 self.config.background_image_enabled,
2890 self.config.background_image.as_deref(),
2891 self.config.background_image_mode,
2892 self.config.background_image_opacity,
2893 );
2894 }
2895
2896 if shader_animation_changed
2897 || shader_enabled_changed
2898 || shader_path_changed
2899 || shader_speed_changed
2900 || shader_full_content_changed
2901 || shader_text_opacity_changed
2902 {
2903 match renderer.set_custom_shader_enabled(
2904 self.config.custom_shader_enabled,
2905 self.config.custom_shader.as_deref(),
2906 self.config.window_opacity,
2907 self.config.custom_shader_text_opacity,
2908 self.config.custom_shader_animation,
2909 self.config.custom_shader_animation_speed,
2910 self.config.custom_shader_full_content,
2911 ) {
2912 Ok(()) => {
2913 self.settings_ui.clear_shader_error();
2914 }
2915 Err(error_msg) => {
2916 log::error!("Shader compilation failed: {}", error_msg);
2917 self.settings_ui.set_shader_error(Some(error_msg));
2918 }
2919 }
2920 }
2921
2922 if cursor_shader_config_changed {
2924 renderer.update_cursor_shader_config(
2925 self.config.cursor_shader_color,
2926 self.config.cursor_shader_trail_duration,
2927 self.config.cursor_shader_glow_radius,
2928 self.config.cursor_shader_glow_intensity,
2929 );
2930 }
2931
2932 if cursor_shader_path_changed
2934 || cursor_shader_enabled_changed
2935 || cursor_shader_animation_changed
2936 || cursor_shader_speed_changed
2937 {
2938 match renderer.set_cursor_shader_enabled(
2939 self.config.cursor_shader_enabled,
2940 self.config.cursor_shader.as_deref(),
2941 self.config.window_opacity,
2942 self.config.cursor_shader_animation,
2943 self.config.cursor_shader_animation_speed,
2944 ) {
2945 Ok(()) => {
2946 self.settings_ui.clear_cursor_shader_error();
2947 }
2948 Err(error_msg) => {
2949 log::error!("Cursor shader compilation failed: {}", error_msg);
2950 self.settings_ui.set_cursor_shader_error(Some(error_msg));
2951 }
2952 }
2953 }
2954
2955 if theme_changed {
2957 if let Some(terminal) = &self.terminal
2958 && let Ok(mut term) = terminal.try_lock()
2959 {
2960 term.set_theme(self.config.load_theme());
2961 log::info!("Applied live theme change: {}", self.config.theme);
2962 }
2963 self.cached_cells = None;
2965 if let Some(window) = &self.window {
2966 window.request_redraw();
2967 }
2968 }
2969
2970 if font_changed {
2971 self.pending_font_rebuild = true;
2973 log::info!("Queued renderer rebuild for font change");
2974 }
2975
2976 if padding_changed {
2978 if let Some((cols, rows)) =
2979 renderer.update_window_padding(self.config.window_padding)
2980 {
2981 let cell_width = renderer.cell_width();
2983 let cell_height = renderer.cell_height();
2984 let width_px = (cols as f32 * cell_width) as usize;
2985 let height_px = (rows as f32 * cell_height) as usize;
2986
2987 if let Some(terminal) = &self.terminal
2988 && let Ok(mut term) = terminal.try_lock()
2989 {
2990 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
2991 log::info!(
2992 "Resized terminal to {}x{} due to padding change",
2993 cols,
2994 rows
2995 );
2996 }
2997 }
2998 log::info!("Updated window padding to {}", self.config.window_padding);
2999 }
3000
3001 self.cached_cells = None;
3003
3004 self.last_applied_opacity = self.config.window_opacity;
3006
3007 if let Some(window) = &self.window {
3009 window.request_redraw();
3010 }
3011 }
3012
3013 if let Some(new_config) = config_to_save {
3015 if let Err(e) = new_config.save() {
3016 log::error!("Failed to save config: {}", e);
3017 } else {
3018 log::info!("Configuration saved successfully");
3019 log::info!(
3020 " Bell settings: sound={}, visual={}, desktop={}",
3021 new_config.notification_bell_sound,
3022 new_config.notification_bell_visual,
3023 new_config.notification_bell_desktop
3024 );
3025 self.settings_ui.update_config(new_config);
3027 }
3028 }
3029 }
3030 let debug_egui_time = egui_start.elapsed();
3031
3032 let avg_frame_time = if !self.debug_frame_times.is_empty() {
3034 self.debug_frame_times.iter().sum::<std::time::Duration>()
3035 / self.debug_frame_times.len() as u32
3036 } else {
3037 std::time::Duration::ZERO
3038 };
3039 let fps = if avg_frame_time.as_secs_f64() > 0.0 {
3040 1.0 / avg_frame_time.as_secs_f64()
3041 } else {
3042 0.0
3043 };
3044
3045 self.fps_value = fps;
3047
3048 if self.debug_frame_times.len() >= 60 {
3050 log::info!(
3051 "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={}",
3052 fps,
3053 avg_frame_time.as_secs_f64() * 1000.0,
3054 self.debug_cell_gen_time.as_secs_f64() * 1000.0,
3055 if self.debug_cache_hit { "HIT" } else { "MISS" },
3056 debug_url_detect_time.as_secs_f64() * 1000.0,
3057 debug_anim_time.as_secs_f64() * 1000.0,
3058 debug_graphics_time.as_secs_f64() * 1000.0,
3059 debug_egui_time.as_secs_f64() * 1000.0,
3060 debug_update_cells_time.as_secs_f64() * 1000.0,
3061 debug_actual_render_time.as_secs_f64() * 1000.0,
3062 self.debug_render_time.as_secs_f64() * 1000.0,
3063 cells.len(),
3064 self.last_generation,
3065 if self.cached_cells.is_some() {
3066 "YES"
3067 } else {
3068 "NO"
3069 }
3070 );
3071 }
3072
3073 let actual_render_start = std::time::Instant::now();
3075 match renderer.render(egui_data, self.settings_ui.visible, show_scrollbar) {
3076 Ok(rendered) => {
3077 if !rendered {
3078 log::trace!("Skipped rendering - no changes");
3079 }
3080 }
3081 Err(e) => {
3082 if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
3085 match surface_error {
3086 SurfaceError::Outdated | SurfaceError::Lost => {
3087 log::warn!(
3088 "Surface error detected ({:?}), reconfiguring...",
3089 surface_error
3090 );
3091 self.force_surface_reconfigure();
3092 }
3093 SurfaceError::Timeout => {
3094 log::warn!("Surface timeout, will retry next frame");
3095 if let Some(window) = &self.window {
3096 window.request_redraw();
3097 }
3098 }
3099 SurfaceError::OutOfMemory => {
3100 log::error!("Surface out of memory: {:?}", surface_error);
3101 }
3102 _ => {
3103 log::error!("Surface error: {:?}", surface_error);
3104 }
3105 }
3106 } else {
3107 log::error!("Render error: {}", e);
3108 }
3109 }
3110 }
3111 debug_actual_render_time = actual_render_start.elapsed();
3112 let _ = debug_actual_render_time;
3113
3114 self.debug_render_time = render_start.elapsed();
3115 }
3116
3117 match pending_clipboard_action {
3120 ClipboardHistoryAction::Paste(content) => {
3121 self.paste_text(&content);
3122 }
3123 ClipboardHistoryAction::ClearAll => {
3124 if let Some(terminal) = &self.terminal
3125 && let Ok(term) = terminal.try_lock()
3126 {
3127 term.clear_all_clipboard_history();
3128 log::info!("Cleared all clipboard history");
3129 }
3130 self.clipboard_history_ui.update_entries(Vec::new());
3131 }
3132 ClipboardHistoryAction::ClearSlot(slot) => {
3133 if let Some(terminal) = &self.terminal
3134 && let Ok(term) = terminal.try_lock()
3135 {
3136 term.clear_clipboard_history(slot);
3137 log::info!("Cleared clipboard history for slot {:?}", slot);
3138 }
3139 }
3140 ClipboardHistoryAction::None => {}
3141 }
3142
3143 let absolute_total = absolute_start.elapsed();
3144 if absolute_total.as_millis() > 10 {
3145 log::debug!(
3146 "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
3147 absolute_total.as_secs_f64() * 1000.0
3148 );
3149 }
3150 }
3151
3152 fn check_notifications(&mut self) {
3153 if let Some(terminal) = &self.terminal
3154 && let Ok(term) = terminal.try_lock()
3155 {
3156 if term.has_notifications() {
3158 let notifications = term.take_notifications();
3159 for notif in notifications {
3160 self.deliver_notification(¬if.title, ¬if.message);
3161 }
3162 }
3163 }
3164 }
3165
3166 fn check_bell(&mut self) {
3167 if self.config.notification_bell_sound == 0
3169 && !self.config.notification_bell_visual
3170 && !self.config.notification_bell_desktop
3171 {
3172 return;
3173 }
3174
3175 if let Some(terminal) = &self.terminal
3176 && let Ok(term) = terminal.try_lock()
3177 {
3178 let current_bell_count = term.bell_count();
3179
3180 if current_bell_count > self.last_bell_count {
3181 let bell_events = current_bell_count - self.last_bell_count;
3183 log::info!("🔔 Bell event detected ({} bell(s))", bell_events);
3184 log::info!(
3185 " Config: sound={}, visual={}, desktop={}",
3186 self.config.notification_bell_sound,
3187 self.config.notification_bell_visual,
3188 self.config.notification_bell_desktop
3189 );
3190
3191 if self.config.notification_bell_sound > 0 {
3193 if let Some(audio_bell) = &self.audio_bell {
3194 log::info!(
3195 " Playing audio bell at {}% volume",
3196 self.config.notification_bell_sound
3197 );
3198 audio_bell.play(self.config.notification_bell_sound);
3199 } else {
3200 log::warn!(" Audio bell requested but not initialized");
3201 }
3202 } else {
3203 log::debug!(" Audio bell disabled (volume=0)");
3204 }
3205
3206 if self.config.notification_bell_visual {
3208 log::info!(" Triggering visual bell flash");
3209 self.visual_bell_flash = Some(std::time::Instant::now());
3210 if let Some(window) = &self.window {
3212 window.request_redraw();
3213 }
3214 } else {
3215 log::debug!(" Visual bell disabled");
3216 }
3217
3218 if self.config.notification_bell_desktop {
3220 log::info!(" Sending desktop notification");
3221 let message = if bell_events == 1 {
3222 "Terminal bell".to_string()
3223 } else {
3224 format!("Terminal bell ({} events)", bell_events)
3225 };
3226 self.deliver_notification("Terminal", &message);
3227 } else {
3228 log::debug!(" Desktop notification disabled");
3229 }
3230
3231 self.last_bell_count = current_bell_count;
3232 }
3233 }
3234 }
3235
3236 fn take_screenshot(&self) {
3237 log::info!("Taking screenshot...");
3238
3239 if let Some(terminal) = &self.terminal {
3240 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
3242 let format = &self.config.screenshot_format;
3243 let filename = format!("par-term_screenshot_{}.{}", timestamp, format);
3244
3245 if let Some(home_dir) = dirs::home_dir() {
3247 let screenshot_dir = home_dir.join("par-term-screenshots");
3248 if !screenshot_dir.exists()
3249 && let Err(e) = std::fs::create_dir_all(&screenshot_dir)
3250 {
3251 log::error!("Failed to create screenshot directory: {}", e);
3252 self.deliver_notification(
3253 "Screenshot Error",
3254 &format!("Failed to create directory: {}", e),
3255 );
3256 return;
3257 }
3258
3259 let path = screenshot_dir.join(&filename);
3260 let path_str = path.to_string_lossy().to_string();
3261
3262 let terminal_clone = Arc::clone(terminal);
3264 let format_clone = format.clone();
3265
3266 let result = std::thread::spawn(move || {
3268 if let Ok(term) = terminal_clone.try_lock() {
3269 term.screenshot_to_file(&path, &format_clone, 0)
3271 } else {
3272 Err(anyhow::anyhow!("Failed to lock terminal"))
3273 }
3274 })
3275 .join();
3276
3277 match result {
3278 Ok(Ok(())) => {
3279 log::info!("Screenshot saved to: {}", path_str);
3280 self.deliver_notification(
3281 "Screenshot Saved",
3282 &format!("Saved to: {}", path_str),
3283 );
3284 }
3285 Ok(Err(e)) => {
3286 log::error!("Failed to save screenshot: {}", e);
3287 self.deliver_notification(
3288 "Screenshot Error",
3289 &format!("Failed to save: {}", e),
3290 );
3291 }
3292 Err(e) => {
3293 log::error!("Screenshot thread panicked: {:?}", e);
3294 self.deliver_notification("Screenshot Error", "Screenshot thread failed");
3295 }
3296 }
3297 } else {
3298 log::error!("Failed to get home directory");
3299 self.deliver_notification("Screenshot Error", "Failed to get home directory");
3300 }
3301 } else {
3302 log::warn!("No terminal available for screenshot");
3303 self.deliver_notification("Screenshot Error", "No terminal available");
3304 }
3305 }
3306
3307 fn toggle_recording(&mut self) {
3310 log::warn!("Recording functionality not yet available in core library");
3311 self.deliver_notification(
3312 "Recording Not Available",
3313 "Recording APIs are not yet implemented in the core library",
3314 );
3315 }
3316
3317 fn deliver_notification(&self, title: &str, message: &str) {
3463 if !title.is_empty() {
3465 log::info!("=== Notification: {} ===", title);
3466 log::info!("{}", message);
3467 log::info!("===========================");
3468 } else {
3469 log::info!("=== Notification ===");
3470 log::info!("{}", message);
3471 log::info!("===================");
3472 }
3473
3474 #[cfg(not(target_os = "macos"))]
3476 {
3477 use notify_rust::Notification;
3478 let notification_title = if !title.is_empty() {
3479 title
3480 } else {
3481 "Terminal Notification"
3482 };
3483
3484 if let Err(e) = Notification::new()
3485 .summary(notification_title)
3486 .body(message)
3487 .timeout(notify_rust::Timeout::Milliseconds(3000))
3488 .show()
3489 {
3490 log::warn!("Failed to send desktop notification: {}", e);
3491 }
3492 }
3493
3494 #[cfg(target_os = "macos")]
3495 {
3496 let notification_title = if !title.is_empty() {
3498 title
3499 } else {
3500 "Terminal Notification"
3501 };
3502
3503 let escaped_title = notification_title.replace('"', "\\\"");
3505 let escaped_message = message.replace('"', "\\\"");
3506
3507 let script = format!(
3509 r#"display notification "{}" with title "{}""#,
3510 escaped_message, escaped_title
3511 );
3512
3513 if let Err(e) = std::process::Command::new("osascript")
3514 .arg("-e")
3515 .arg(&script)
3516 .output()
3517 {
3518 log::warn!("Failed to send macOS desktop notification: {}", e);
3519 }
3520 }
3521 }
3522
3523 fn update_window_title_with_shell_integration(&self) {
3526 if self.scroll_state.offset != 0 {
3528 return;
3529 }
3530
3531 if self.hovered_url.is_some() {
3533 return;
3534 }
3535
3536 let window = if let Some(w) = &self.window {
3538 w
3539 } else {
3540 return;
3541 };
3542
3543 let terminal = if let Some(t) = &self.terminal {
3545 t
3546 } else {
3547 return;
3548 };
3549
3550 if let Ok(term) = terminal.try_lock() {
3552 let mut title_parts = vec![self.config.window_title.clone()];
3553
3554 if let Some(cwd) = term.shell_integration_cwd() {
3556 let abbreviated_cwd = if let Some(home) = dirs::home_dir() {
3558 cwd.replace(&home.to_string_lossy().to_string(), "~")
3559 } else {
3560 cwd
3561 };
3562 title_parts.push(format!("({})", abbreviated_cwd));
3563 }
3564
3565 if let Some(exit_code) = term.shell_integration_exit_code()
3567 && exit_code != 0
3568 {
3569 title_parts.push(format!("[Exit: {}]", exit_code));
3570 }
3571
3572 if self.is_recording {
3574 title_parts.push("[RECORDING]".to_string());
3575 }
3576
3577 let title = title_parts.join(" ");
3579 window.set_title(&title);
3580 }
3581 }
3582}
3583
3584impl ApplicationHandler for AppState {
3585 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
3586 if self.window.is_none() {
3587 let mut window_attrs = Window::default_attributes()
3588 .with_title(&self.config.window_title)
3589 .with_inner_size(winit::dpi::LogicalSize::new(
3590 self.config.window_width,
3591 self.config.window_height,
3592 ))
3593 .with_decorations(self.config.window_decorations);
3594
3595 let icon_bytes = include_bytes!("../assets/icon.png");
3597 if let Ok(icon_image) = image::load_from_memory(icon_bytes) {
3598 let rgba = icon_image.to_rgba8();
3599 let (width, height) = rgba.dimensions();
3600 if let Ok(icon) = winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
3601 window_attrs = window_attrs.with_window_icon(Some(icon));
3602 log::info!("Window icon set ({}x{})", width, height);
3603 } else {
3604 log::warn!("Failed to create window icon from RGBA data");
3605 }
3606 } else {
3607 log::warn!("Failed to load embedded icon image");
3608 }
3609
3610 if self.config.window_always_on_top {
3612 window_attrs =
3613 window_attrs.with_window_level(winit::window::WindowLevel::AlwaysOnTop);
3614 log::info!("Window always-on-top enabled");
3615 }
3616
3617 window_attrs = window_attrs.with_transparent(true);
3620 log::info!(
3621 "Window transparency enabled (opacity: {})",
3622 self.config.window_opacity
3623 );
3624
3625 match event_loop.create_window(window_attrs) {
3626 Ok(window) => {
3627 let runtime = Arc::clone(&self.runtime);
3629 if let Err(e) = runtime.block_on(self.initialize_async(window)) {
3630 log::error!("Failed to initialize: {}", e);
3631 event_loop.exit();
3632 }
3633 }
3634 Err(e) => {
3635 log::error!("Failed to create window: {}", e);
3636 event_loop.exit();
3637 }
3638 }
3639 }
3640 }
3641
3642 fn window_event(
3643 &mut self,
3644 event_loop: &ActiveEventLoop,
3645 _window_id: WindowId,
3646 event: WindowEvent,
3647 ) {
3648 use winit::keyboard::{Key, NamedKey};
3649
3650 if let WindowEvent::KeyboardInput {
3652 event: key_event, ..
3653 } = &event
3654 {
3655 match &key_event.logical_key {
3656 Key::Character(s) => {
3657 log::trace!(
3658 "window_event: Character '{}', state={:?}",
3659 s,
3660 key_event.state
3661 );
3662 }
3663 Key::Named(NamedKey::Space) => {
3664 log::debug!("🔔 SPACE EVENT: state={:?}", key_event.state);
3665 }
3666 Key::Named(named) => {
3667 log::trace!(
3668 "window_event: Named key {:?}, state={:?}",
3669 named,
3670 key_event.state
3671 );
3672 }
3673 other => {
3674 log::trace!(
3675 "window_event: Other key {:?}, state={:?}",
3676 other,
3677 key_event.state
3678 );
3679 }
3680 }
3681 }
3682
3683 let egui_consumed =
3685 if let (Some(egui_state), Some(window)) = (&mut self.egui_state, &self.window) {
3686 let event_response = egui_state.on_window_event(window, &event);
3687 event_response.consumed
3688 } else {
3689 false
3690 };
3691
3692 if egui_consumed
3694 && !self.settings_ui.visible
3695 && let WindowEvent::KeyboardInput {
3696 event: key_event, ..
3697 } = &event
3698 && let Key::Named(NamedKey::Space) = &key_event.logical_key
3699 {
3700 log::debug!("egui tried to consume Space (UI closed, ignoring)");
3701 }
3702
3703 let any_ui_visible =
3706 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
3707 if egui_consumed
3708 && any_ui_visible
3709 && !matches!(
3710 event,
3711 WindowEvent::CloseRequested | WindowEvent::RedrawRequested
3712 )
3713 {
3714 if let WindowEvent::KeyboardInput {
3715 event: key_event, ..
3716 } = &event
3717 {
3718 match &key_event.logical_key {
3719 Key::Named(NamedKey::Space) => {
3720 log::debug!("egui consumed Space while UI panel is visible")
3721 }
3722 Key::Named(_) => {
3723 log::debug!("egui consumed named key while UI panel is visible")
3724 }
3725 _ => {}
3726 }
3727 }
3728 return;
3729 }
3730
3731 match event {
3732 WindowEvent::CloseRequested => {
3733 log::info!("Close requested, cleaning up and exiting");
3734 self.is_shutting_down = true;
3736 if let Some(task) = self.refresh_task.take() {
3738 task.abort();
3739 log::info!("Refresh task aborted");
3740 }
3741 event_loop.exit();
3742 }
3743
3744 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
3745 if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
3746 log::info!(
3747 "Scale factor changed to {} (display change detected)",
3748 scale_factor
3749 );
3750
3751 let size = window.inner_size();
3752 let (cols, rows) = renderer.handle_scale_factor_change(scale_factor, size);
3753
3754 renderer.reconfigure_surface();
3757
3758 let cell_width = renderer.cell_width();
3760 let cell_height = renderer.cell_height();
3761 let width_px = (cols as f32 * cell_width) as usize;
3762 let height_px = (rows as f32 * cell_height) as usize;
3763
3764 if let Some(terminal) = &self.terminal
3766 && let Ok(mut term) = terminal.try_lock()
3767 {
3768 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
3769 }
3770
3771 #[cfg(target_os = "macos")]
3773 {
3774 if let Err(e) =
3775 crate::macos_metal::configure_metal_layer_for_performance(window)
3776 {
3777 log::warn!(
3778 "Failed to reconfigure Metal layer after display change: {}",
3779 e
3780 );
3781 }
3782 }
3783
3784 window.request_redraw();
3786 }
3787 }
3788
3789 WindowEvent::Moved(_) => {
3791 if let (Some(renderer), Some(window)) = (&mut self.renderer, &self.window) {
3792 log::debug!(
3793 "Window moved - reconfiguring surface for potential display change"
3794 );
3795
3796 renderer.reconfigure_surface();
3799
3800 #[cfg(target_os = "macos")]
3802 {
3803 if let Err(e) =
3804 crate::macos_metal::configure_metal_layer_for_performance(window)
3805 {
3806 log::warn!(
3807 "Failed to reconfigure Metal layer after window move: {}",
3808 e
3809 );
3810 }
3811 }
3812
3813 window.request_redraw();
3815 }
3816 }
3817
3818 WindowEvent::Resized(physical_size) => {
3819 if let Some(renderer) = &mut self.renderer {
3820 let (cols, rows) = renderer.resize(physical_size);
3821
3822 let cell_width = renderer.cell_width();
3824 let cell_height = renderer.cell_height();
3825 let width_px = (cols as f32 * cell_width) as usize;
3826 let height_px = (rows as f32 * cell_height) as usize;
3827
3828 if let Some(terminal) = &self.terminal
3833 && let Ok(mut term) = terminal.try_lock()
3834 {
3835 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
3836 self.cached_scrollback_len = term.scrollback_len();
3837
3838 let total_lines = rows + self.cached_scrollback_len;
3840 renderer.update_scrollbar(self.scroll_state.offset, rows, total_lines);
3841 }
3842
3843 self.cached_cells = None;
3845 }
3846 }
3847
3848 WindowEvent::KeyboardInput { event, .. } => {
3849 self.handle_key_event(event, event_loop);
3850 }
3851
3852 WindowEvent::ModifiersChanged(modifiers) => {
3853 self.input_handler.update_modifiers(modifiers);
3854 }
3855
3856 WindowEvent::MouseWheel { delta, .. } => {
3857 if !self.is_egui_using_pointer() {
3859 self.handle_mouse_wheel(delta);
3860 }
3861 }
3862
3863 WindowEvent::MouseInput { button, state, .. } => {
3864 if !self.is_egui_using_pointer() {
3866 self.handle_mouse_button(button, state);
3867 }
3868 }
3869
3870 WindowEvent::CursorMoved { position, .. } => {
3871 if !self.is_egui_using_pointer() {
3873 self.handle_mouse_move((position.x, position.y));
3874 }
3875 }
3876
3877 WindowEvent::RedrawRequested => {
3878 if self.is_shutting_down {
3880 return;
3881 }
3882
3883 if self.config.exit_on_shell_exit
3885 && let Some(terminal) = &self.terminal
3886 && let Ok(term) = terminal.try_lock()
3887 && !term.is_running()
3888 {
3889 log::info!("Shell has exited, closing terminal");
3890 self.is_shutting_down = true;
3892 if let Some(task) = self.refresh_task.take() {
3894 task.abort();
3895 log::info!("Refresh task aborted");
3896 }
3897 event_loop.exit();
3898 return;
3899 }
3900
3901 self.render();
3902 }
3903
3904 _ => {}
3905 }
3906 }
3907
3908 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
3909 if self.is_shutting_down {
3911 return;
3912 }
3913
3914 self.check_notifications();
3916
3917 self.check_bell();
3919
3920 self.update_window_title_with_shell_integration();
3922
3923 let now = std::time::Instant::now();
3929 let mut next_wake = now + std::time::Duration::from_secs(1); if self.config.cursor_blink {
3934 if self.cursor_blink_timer.is_none() {
3935 let blink_interval =
3936 std::time::Duration::from_millis(self.config.cursor_blink_interval);
3937 self.cursor_blink_timer = Some(now + blink_interval);
3938 }
3939
3940 if let Some(next_blink) = self.cursor_blink_timer {
3941 if now >= next_blink {
3942 self.needs_redraw = true;
3944 let blink_interval =
3945 std::time::Duration::from_millis(self.config.cursor_blink_interval);
3946 self.cursor_blink_timer = Some(now + blink_interval);
3947 } else if next_blink < next_wake {
3948 next_wake = next_blink;
3950 }
3951 }
3952 }
3953
3954 if self.scroll_state.animation_start.is_some() {
3957 self.needs_redraw = true;
3958 let next_frame = now + std::time::Duration::from_millis(16);
3959 if next_frame < next_wake {
3960 next_wake = next_frame;
3961 }
3962 }
3963
3964 if self.visual_bell_flash.is_some() {
3967 self.needs_redraw = true;
3968 let next_frame = now + std::time::Duration::from_millis(16);
3969 if next_frame < next_wake {
3970 next_wake = next_frame;
3971 }
3972 }
3973
3974 if (self.is_selecting || self.selection.is_some() || self.scroll_state.dragging)
3977 && self.mouse_button_pressed
3978 {
3979 self.needs_redraw = true;
3980 }
3981
3982 if let Some(renderer) = &self.renderer
3985 && renderer.needs_continuous_render()
3986 {
3987 self.needs_redraw = true;
3988 let next_frame = now + std::time::Duration::from_millis(16);
3989 if next_frame < next_wake {
3990 next_wake = next_frame;
3991 }
3992 }
3993
3994 if self.needs_redraw
3997 && let Some(window) = &self.window
3998 {
3999 window.request_redraw();
4000 self.needs_redraw = false;
4001 }
4002
4003 event_loop.set_control_flow(ControlFlow::WaitUntil(next_wake));
4005 }
4006}
4007
4008impl Drop for AppState {
4009 fn drop(&mut self) {
4010 log::info!("Shutting down application");
4011
4012 self.is_shutting_down = true;
4014
4015 if let Some(handle) = self.refresh_task.take() {
4017 handle.abort();
4018 log::info!("Refresh task aborted");
4019
4020 std::thread::sleep(std::time::Duration::from_millis(100));
4022 }
4023
4024 if let Some(terminal) = &self.terminal {
4026 let killed = if let Ok(mut term) = terminal.try_lock() {
4028 if term.is_running() {
4029 log::info!("Killing PTY process during shutdown");
4030 match term.kill() {
4031 Ok(()) => {
4032 log::info!("PTY process killed successfully");
4033 true
4034 }
4035 Err(e) => {
4036 log::warn!("Failed to kill PTY process: {:?}", e);
4037 false
4038 }
4039 }
4040 } else {
4041 log::info!("PTY process already stopped");
4042 true
4043 }
4044 } else {
4045 log::warn!("Could not acquire terminal lock to kill PTY during shutdown");
4046 false
4047 };
4048
4049 if killed {
4051 std::thread::sleep(std::time::Duration::from_millis(100));
4052 }
4053 }
4054
4055 if let Some(terminal) = self.terminal.take() {
4057 let timeout = std::time::Duration::from_millis(500);
4059 let start = std::time::Instant::now();
4060
4061 loop {
4062 if let Ok(_term) = terminal.try_lock() {
4063 log::info!("Terminal lock acquired for cleanup");
4064 break;
4066 } else if start.elapsed() >= timeout {
4067 log::warn!(
4068 "Could not acquire terminal lock within timeout during shutdown, forcing cleanup"
4069 );
4070 break;
4072 }
4073 std::thread::sleep(std::time::Duration::from_millis(10));
4074 }
4075 }
4077
4078 log::info!("Application shutdown complete");
4079 }
4080}