1use crate::app::debug_state::DebugState;
7use crate::clipboard_history_ui::{ClipboardHistoryAction, ClipboardHistoryUI};
8use crate::config::Config;
9use crate::help_ui::HelpUI;
10use crate::input::InputHandler;
11use crate::renderer::Renderer;
12use crate::selection::SelectionMode;
13use crate::settings_ui::{CursorShaderEditorResult, SettingsUI, ShaderEditorResult};
14use crate::tab::TabManager;
15use crate::tab_bar_ui::{TabBarAction, TabBarUI};
16use anyhow::Result;
17use std::sync::Arc;
18use tokio::runtime::Runtime;
19use wgpu::SurfaceError;
20use winit::event::KeyEvent;
21use winit::window::Window;
22
23pub struct WindowState {
25 pub(crate) config: Config,
26 pub(crate) window: Option<Arc<Window>>,
27 pub(crate) renderer: Option<Renderer>,
28 pub(crate) input_handler: InputHandler,
29 pub(crate) runtime: Arc<Runtime>,
30
31 pub(crate) tab_manager: TabManager,
33 pub(crate) tab_bar_ui: TabBarUI,
35
36 pub(crate) debug: DebugState,
37
38 pub(crate) cursor_opacity: f32,
40 pub(crate) last_cursor_blink: Option<std::time::Instant>,
42 pub(crate) last_key_press: Option<std::time::Instant>,
44 pub(crate) is_fullscreen: bool,
46 pub(crate) egui_ctx: Option<egui::Context>,
48 pub(crate) egui_state: Option<egui_winit::State>,
50 pub(crate) settings_ui: SettingsUI,
52 pub(crate) help_ui: HelpUI,
54 pub(crate) clipboard_history_ui: ClipboardHistoryUI,
56 pub(crate) is_recording: bool,
58 #[allow(dead_code)]
60 pub(crate) recording_start_time: Option<std::time::Instant>,
61 pub(crate) is_shutting_down: bool,
63
64 pub(crate) needs_redraw: bool,
67 pub(crate) cursor_blink_timer: Option<std::time::Instant>,
69 pub(crate) pending_font_rebuild: bool,
71}
72
73impl WindowState {
74 pub fn new(config: Config, runtime: Arc<Runtime>) -> Self {
76 let settings_ui = SettingsUI::new(config.clone());
77
78 Self {
79 config,
80 window: None,
81 renderer: None,
82 input_handler: InputHandler::new(),
83 runtime,
84
85 tab_manager: TabManager::new(),
86 tab_bar_ui: TabBarUI::new(),
87
88 debug: DebugState::new(),
89
90 cursor_opacity: 1.0,
91 last_cursor_blink: None,
92 last_key_press: None,
93 is_fullscreen: false,
94 egui_ctx: None,
95 egui_state: None,
96 settings_ui,
97 help_ui: HelpUI::new(),
98 clipboard_history_ui: ClipboardHistoryUI::new(),
99 is_recording: false,
100 recording_start_time: None,
101 is_shutting_down: false,
102
103 needs_redraw: true,
104 cursor_blink_timer: None,
105 pending_font_rebuild: false,
106 }
107 }
108
109 pub fn new_tab(&mut self) {
115 if self.config.max_tabs > 0 && self.tab_manager.tab_count() >= self.config.max_tabs {
117 log::warn!(
118 "Cannot create new tab: max_tabs limit ({}) reached",
119 self.config.max_tabs
120 );
121 return;
122 }
123
124 match self.tab_manager.new_tab(
125 &self.config,
126 Arc::clone(&self.runtime),
127 self.config.tab_inherit_cwd,
128 ) {
129 Ok(tab_id) => {
130 if let Some(window) = &self.window
132 && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
133 {
134 tab.start_refresh_task(
135 Arc::clone(&self.runtime),
136 Arc::clone(window),
137 self.config.max_fps,
138 );
139
140 if let Some(renderer) = &self.renderer
142 && let Ok(mut term) = tab.terminal.try_lock()
143 {
144 let (cols, rows) = renderer.grid_size();
145 let size = renderer.size();
146 let width_px = size.width as usize;
147 let height_px = size.height as usize;
148
149 term.set_cell_dimensions(
151 renderer.cell_width() as u32,
152 renderer.cell_height() as u32,
153 );
154
155 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
157 log::info!(
158 "Resized new tab {} terminal to {}x{} ({}x{} px)",
159 tab_id,
160 cols,
161 rows,
162 width_px,
163 height_px
164 );
165 }
166 }
167
168 self.needs_redraw = true;
169 if let Some(window) = &self.window {
170 window.request_redraw();
171 }
172 }
173 Err(e) => {
174 log::error!("Failed to create new tab: {}", e);
175 }
176 }
177 }
178
179 pub fn close_current_tab(&mut self) -> bool {
182 if let Some(tab_id) = self.tab_manager.active_tab_id() {
183 let is_last = self.tab_manager.close_tab(tab_id);
184 self.needs_redraw = true;
185 if let Some(window) = &self.window {
186 window.request_redraw();
187 }
188 is_last
189 } else {
190 true }
192 }
193
194 pub fn next_tab(&mut self) {
196 self.tab_manager.next_tab();
197 if let Some(renderer) = &mut self.renderer {
199 renderer.clear_all_cells();
200 }
201 if let Some(tab) = self.tab_manager.active_tab_mut() {
202 tab.cache.cells = None;
203 }
204 self.needs_redraw = true;
205 if let Some(window) = &self.window {
206 window.request_redraw();
207 }
208 }
209
210 pub fn prev_tab(&mut self) {
212 self.tab_manager.prev_tab();
213 if let Some(renderer) = &mut self.renderer {
215 renderer.clear_all_cells();
216 }
217 if let Some(tab) = self.tab_manager.active_tab_mut() {
218 tab.cache.cells = None;
219 }
220 self.needs_redraw = true;
221 if let Some(window) = &self.window {
222 window.request_redraw();
223 }
224 }
225
226 pub fn switch_to_tab_index(&mut self, index: usize) {
228 self.tab_manager.switch_to_index(index);
229 if let Some(renderer) = &mut self.renderer {
231 renderer.clear_all_cells();
232 }
233 if let Some(tab) = self.tab_manager.active_tab_mut() {
234 tab.cache.cells = None;
235 }
236 self.needs_redraw = true;
237 if let Some(window) = &self.window {
238 window.request_redraw();
239 }
240 }
241
242 pub fn move_tab_left(&mut self) {
244 self.tab_manager.move_active_tab_left();
245 self.needs_redraw = true;
246 if let Some(window) = &self.window {
247 window.request_redraw();
248 }
249 }
250
251 pub fn move_tab_right(&mut self) {
253 self.tab_manager.move_active_tab_right();
254 self.needs_redraw = true;
255 if let Some(window) = &self.window {
256 window.request_redraw();
257 }
258 }
259
260 pub fn duplicate_tab(&mut self) {
262 match self
263 .tab_manager
264 .duplicate_active_tab(&self.config, Arc::clone(&self.runtime))
265 {
266 Ok(Some(tab_id)) => {
267 if let Some(window) = &self.window
269 && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
270 {
271 tab.start_refresh_task(
272 Arc::clone(&self.runtime),
273 Arc::clone(window),
274 self.config.max_fps,
275 );
276 }
277 self.needs_redraw = true;
278 if let Some(window) = &self.window {
279 window.request_redraw();
280 }
281 }
282 Ok(None) => {
283 log::debug!("No active tab to duplicate");
284 }
285 Err(e) => {
286 log::error!("Failed to duplicate tab: {}", e);
287 }
288 }
289 }
290
291 pub fn has_multiple_tabs(&self) -> bool {
293 self.tab_manager.has_multiple_tabs()
294 }
295
296 #[allow(dead_code)]
298 pub fn active_terminal(
299 &self,
300 ) -> Option<&Arc<tokio::sync::Mutex<crate::terminal::TerminalManager>>> {
301 self.tab_manager.active_tab().map(|tab| &tab.terminal)
302 }
303
304 #[allow(dead_code)]
308 pub(crate) fn terminal(
309 &self,
310 ) -> Option<&Arc<tokio::sync::Mutex<crate::terminal::TerminalManager>>> {
311 self.active_terminal()
312 }
313
314 #[allow(dead_code)]
315 pub(crate) fn scroll_state(&self) -> Option<&crate::scroll_state::ScrollState> {
316 self.tab_manager.active_tab().map(|t| &t.scroll_state)
317 }
318
319 #[allow(dead_code)]
320 pub(crate) fn scroll_state_mut(&mut self) -> Option<&mut crate::scroll_state::ScrollState> {
321 self.tab_manager
322 .active_tab_mut()
323 .map(|t| &mut t.scroll_state)
324 }
325
326 #[allow(dead_code)]
327 pub(crate) fn mouse(&self) -> Option<&crate::app::mouse::MouseState> {
328 self.tab_manager.active_tab().map(|t| &t.mouse)
329 }
330
331 #[allow(dead_code)]
332 pub(crate) fn mouse_mut(&mut self) -> Option<&mut crate::app::mouse::MouseState> {
333 self.tab_manager.active_tab_mut().map(|t| &mut t.mouse)
334 }
335
336 #[allow(dead_code)]
337 pub(crate) fn bell(&self) -> Option<&crate::app::bell::BellState> {
338 self.tab_manager.active_tab().map(|t| &t.bell)
339 }
340
341 #[allow(dead_code)]
342 pub(crate) fn bell_mut(&mut self) -> Option<&mut crate::app::bell::BellState> {
343 self.tab_manager.active_tab_mut().map(|t| &mut t.bell)
344 }
345
346 #[allow(dead_code)]
347 pub(crate) fn cache(&self) -> Option<&crate::app::render_cache::RenderCache> {
348 self.tab_manager.active_tab().map(|t| &t.cache)
349 }
350
351 #[allow(dead_code)]
352 pub(crate) fn cache_mut(&mut self) -> Option<&mut crate::app::render_cache::RenderCache> {
353 self.tab_manager.active_tab_mut().map(|t| &mut t.cache)
354 }
355
356 #[allow(dead_code)]
357 pub(crate) fn refresh_task(&self) -> Option<&Option<tokio::task::JoinHandle<()>>> {
358 self.tab_manager.active_tab().map(|t| &t.refresh_task)
359 }
360
361 #[allow(dead_code)]
362 pub(crate) fn abort_refresh_task(&mut self) {
363 if let Some(tab) = self.tab_manager.active_tab_mut()
364 && let Some(task) = tab.refresh_task.take()
365 {
366 task.abort();
367 }
368 }
369
370 pub(crate) fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
372 let mut extracted = String::new();
373 let end_bound = end_col.unwrap_or(usize::MAX);
374
375 if start_col > end_bound {
376 return extracted;
377 }
378
379 for (idx, ch) in line.chars().enumerate() {
380 if idx > end_bound {
381 break;
382 }
383
384 if idx >= start_col {
385 extracted.push(ch);
386 }
387 }
388
389 extracted
390 }
391
392 pub(crate) fn rebuild_renderer(&mut self) -> Result<()> {
394 let window = if let Some(w) = &self.window {
395 Arc::clone(w)
396 } else {
397 return Ok(()); };
399
400 let theme = self.config.load_theme();
401 let font_family = if self.config.font_family.is_empty() {
402 None
403 } else {
404 Some(self.config.font_family.as_str())
405 };
406
407 let mut renderer = self.runtime.block_on(Renderer::new(
408 Arc::clone(&window),
409 font_family,
410 self.config.font_family_bold.as_deref(),
411 self.config.font_family_italic.as_deref(),
412 self.config.font_family_bold_italic.as_deref(),
413 &self.config.font_ranges,
414 self.config.font_size,
415 self.config.window_padding,
416 self.config.line_spacing,
417 self.config.char_spacing,
418 &self.config.scrollbar_position,
419 self.config.scrollbar_width,
420 self.config.scrollbar_thumb_color,
421 self.config.scrollbar_track_color,
422 self.config.enable_text_shaping,
423 self.config.enable_ligatures,
424 self.config.enable_kerning,
425 self.config.vsync_mode,
426 self.config.window_opacity,
427 theme.background.as_array(),
428 self.config.background_image.as_deref(),
429 self.config.background_image_enabled,
430 self.config.background_image_mode,
431 self.config.background_image_opacity,
432 self.config.custom_shader.as_deref(),
433 self.config.custom_shader_enabled,
434 self.config.custom_shader_animation,
435 self.config.custom_shader_animation_speed,
436 self.config.custom_shader_text_opacity,
437 self.config.custom_shader_full_content,
438 self.config.custom_shader_brightness,
439 &self.config.shader_channel_paths(),
441 self.config.cursor_shader.as_deref(),
443 self.config.cursor_shader_enabled,
444 self.config.cursor_shader_animation,
445 self.config.cursor_shader_animation_speed,
446 ))?;
447
448 let (cols, rows) = renderer.grid_size();
449 let cell_width = renderer.cell_width();
450 let cell_height = renderer.cell_height();
451 let width_px = (cols as f32 * cell_width) as usize;
452 let height_px = (rows as f32 * cell_height) as usize;
453
454 for tab in self.tab_manager.tabs_mut() {
456 if let Ok(mut term) = tab.terminal.try_lock() {
457 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
458 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
459 term.set_theme(self.config.load_theme());
460 }
461 tab.cache.cells = None;
462 }
463
464 renderer.update_cursor_shader_config(
466 self.config.cursor_shader_color,
467 self.config.cursor_shader_trail_duration,
468 self.config.cursor_shader_glow_radius,
469 self.config.cursor_shader_glow_intensity,
470 );
471
472 renderer.update_cursor_color(self.config.cursor_color);
474
475 renderer.set_cursor_hidden_for_shader(
477 self.config.cursor_shader_enabled && self.config.cursor_shader_hides_cursor,
478 );
479
480 self.renderer = Some(renderer);
481 self.needs_redraw = true;
482
483 let previous_memory = self
488 .egui_ctx
489 .as_ref()
490 .map(|ctx| ctx.memory(|mem| mem.clone()));
491
492 let scale_factor = window.scale_factor() as f32;
493 let egui_ctx = egui::Context::default();
494 if let Some(memory) = previous_memory {
495 egui_ctx.memory_mut(|mem| *mem = memory);
496 }
497 let egui_state = egui_winit::State::new(
498 egui_ctx.clone(),
499 egui::ViewportId::ROOT,
500 &window,
501 Some(scale_factor),
502 None,
503 None,
504 );
505 self.egui_ctx = Some(egui_ctx);
506 self.egui_state = Some(egui_state);
507
508 if let Some(window) = &self.window {
509 window.request_redraw();
510 }
511
512 Ok(())
513 }
514
515 pub(crate) async fn initialize_async(&mut self, window: Window) -> Result<()> {
517 window.set_ime_allowed(true);
519 log::debug!("IME enabled for character input");
520
521 let window = Arc::new(window);
522
523 let egui_ctx = egui::Context::default();
525 let egui_state = egui_winit::State::new(
526 egui_ctx.clone(),
527 egui::ViewportId::ROOT,
528 &window,
529 Some(window.scale_factor() as f32),
530 None,
531 None, );
533 self.egui_ctx = Some(egui_ctx);
534 self.egui_state = Some(egui_state);
535
536 let font_family = if self.config.font_family.is_empty() {
538 None
539 } else {
540 Some(self.config.font_family.as_str())
541 };
542 let theme = self.config.load_theme();
543 let mut renderer = Renderer::new(
544 Arc::clone(&window),
545 font_family,
546 self.config.font_family_bold.as_deref(),
547 self.config.font_family_italic.as_deref(),
548 self.config.font_family_bold_italic.as_deref(),
549 &self.config.font_ranges,
550 self.config.font_size,
551 self.config.window_padding,
552 self.config.line_spacing,
553 self.config.char_spacing,
554 &self.config.scrollbar_position,
555 self.config.scrollbar_width,
556 self.config.scrollbar_thumb_color,
557 self.config.scrollbar_track_color,
558 self.config.enable_text_shaping,
559 self.config.enable_ligatures,
560 self.config.enable_kerning,
561 self.config.vsync_mode,
562 self.config.window_opacity,
563 theme.background.as_array(),
564 self.config.background_image.as_deref(),
565 self.config.background_image_enabled,
566 self.config.background_image_mode,
567 self.config.background_image_opacity,
568 self.config.custom_shader.as_deref(),
569 self.config.custom_shader_enabled,
570 self.config.custom_shader_animation,
571 self.config.custom_shader_animation_speed,
572 self.config.custom_shader_text_opacity,
573 self.config.custom_shader_full_content,
574 self.config.custom_shader_brightness,
575 &self.config.shader_channel_paths(),
577 self.config.cursor_shader.as_deref(),
579 self.config.cursor_shader_enabled,
580 self.config.cursor_shader_animation,
581 self.config.cursor_shader_animation_speed,
582 )
583 .await?;
584
585 #[cfg(target_os = "macos")]
590 {
591 if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
592 log::warn!("Failed to configure Metal layer: {}", e);
593 log::warn!(
594 "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
595 );
596 }
597 if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
599 log::warn!("Failed to set initial Metal layer opacity: {}", e);
600 }
601 }
602
603 renderer.update_cursor_shader_config(
605 self.config.cursor_shader_color,
606 self.config.cursor_shader_trail_duration,
607 self.config.cursor_shader_glow_radius,
608 self.config.cursor_shader_glow_intensity,
609 );
610
611 renderer.update_cursor_color(self.config.cursor_color);
613
614 renderer.set_cursor_hidden_for_shader(
616 self.config.cursor_shader_enabled && self.config.cursor_shader_hides_cursor,
617 );
618
619 self.window = Some(Arc::clone(&window));
620 self.renderer = Some(renderer);
621
622 let tab_id = self.tab_manager.new_tab(
624 &self.config,
625 Arc::clone(&self.runtime),
626 false, )?;
628
629 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
631 if let Some(renderer) = &self.renderer {
632 let (renderer_cols, renderer_rows) = renderer.grid_size();
633 let cell_width = renderer.cell_width();
634 let cell_height = renderer.cell_height();
635 let width_px = (renderer_cols as f32 * cell_width) as usize;
636 let height_px = (renderer_rows as f32 * cell_height) as usize;
637
638 if let Ok(mut term) = tab.terminal.try_lock() {
639 let _ =
640 term.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px);
641 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
642 log::info!(
643 "Initial terminal dimensions: {}x{} ({}x{} px)",
644 renderer_cols,
645 renderer_rows,
646 width_px,
647 height_px
648 );
649 }
650 }
651
652 tab.start_refresh_task(
654 Arc::clone(&self.runtime),
655 Arc::clone(&window),
656 self.config.max_fps,
657 );
658 }
659
660 Ok(())
661 }
662
663 pub(crate) fn force_surface_reconfigure(&mut self) {
667 log::info!("Force surface reconfigure triggered");
668
669 if let Some(renderer) = &mut self.renderer {
670 renderer.reconfigure_surface();
672
673 renderer.clear_glyph_cache();
675
676 if let Some(tab) = self.tab_manager.active_tab_mut() {
678 tab.cache.cells = None;
679 }
680 }
681
682 #[cfg(target_os = "macos")]
684 {
685 if let Some(window) = &self.window
686 && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
687 {
688 log::warn!("Failed to reconfigure Metal layer: {}", e);
689 }
690 }
691
692 if let Some(window) = &self.window {
694 window.request_redraw();
695 }
696
697 self.needs_redraw = true;
698 }
699
700 pub(crate) fn handle_fullscreen_toggle(&mut self, event: &KeyEvent) -> bool {
701 use winit::event::ElementState;
702 use winit::keyboard::{Key, NamedKey};
703
704 if event.state != ElementState::Pressed {
705 return false;
706 }
707
708 if matches!(event.logical_key, Key::Named(NamedKey::F11))
710 && let Some(window) = &self.window
711 {
712 self.is_fullscreen = !self.is_fullscreen;
713
714 if self.is_fullscreen {
715 window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
716 log::info!("Entering fullscreen mode");
717 } else {
718 window.set_fullscreen(None);
719 log::info!("Exiting fullscreen mode");
720 }
721
722 return true;
723 }
724
725 false
726 }
727
728 pub(crate) fn handle_settings_toggle(&mut self, event: &KeyEvent) -> bool {
729 use winit::event::ElementState;
730 use winit::keyboard::{Key, NamedKey};
731
732 if event.state != ElementState::Pressed {
733 return false;
734 }
735
736 if matches!(event.logical_key, Key::Named(NamedKey::F12)) {
738 self.settings_ui.toggle();
739 log::info!(
740 "Settings UI toggled: {}",
741 if self.settings_ui.visible {
742 "visible"
743 } else {
744 "hidden"
745 }
746 );
747
748 if let Some(window) = &self.window {
750 window.request_redraw();
751 }
752
753 return true;
754 }
755
756 false
757 }
758
759 pub(crate) fn handle_help_toggle(&mut self, event: &KeyEvent) -> bool {
761 use winit::event::ElementState;
762 use winit::keyboard::{Key, NamedKey};
763
764 if event.state != ElementState::Pressed {
765 return false;
766 }
767
768 if matches!(event.logical_key, Key::Named(NamedKey::F1)) {
770 self.help_ui.toggle();
771 log::info!(
772 "Help UI toggled: {}",
773 if self.help_ui.visible {
774 "visible"
775 } else {
776 "hidden"
777 }
778 );
779
780 if let Some(window) = &self.window {
782 window.request_redraw();
783 }
784
785 return true;
786 }
787
788 if matches!(event.logical_key, Key::Named(NamedKey::Escape)) && self.help_ui.visible {
790 self.help_ui.visible = false;
791 log::info!("Help UI closed via Escape");
792
793 if let Some(window) = &self.window {
794 window.request_redraw();
795 }
796
797 return true;
798 }
799
800 false
801 }
802
803 pub(crate) fn handle_shader_editor_toggle(&mut self, event: &KeyEvent) -> bool {
805 use winit::event::ElementState;
806 use winit::keyboard::{Key, NamedKey};
807
808 if event.state != ElementState::Pressed {
809 return false;
810 }
811
812 if matches!(event.logical_key, Key::Named(NamedKey::F11)) {
814 if self.settings_ui.is_shader_editor_visible() {
815 log::info!("Shader editor close requested via F11");
817 } else {
818 if self.settings_ui.open_shader_editor() {
820 log::info!("Shader editor opened via F11");
821 } else {
822 log::warn!("Cannot open shader editor: no shader path configured in settings");
823 }
824 }
825
826 if let Some(window) = &self.window {
828 window.request_redraw();
829 }
830
831 return true;
832 }
833
834 false
835 }
836
837 pub(crate) fn handle_fps_overlay_toggle(&mut self, event: &KeyEvent) -> bool {
839 use winit::event::ElementState;
840 use winit::keyboard::{Key, NamedKey};
841
842 if event.state != ElementState::Pressed {
843 return false;
844 }
845
846 if matches!(event.logical_key, Key::Named(NamedKey::F3)) {
848 self.debug.show_fps_overlay = !self.debug.show_fps_overlay;
849 log::info!(
850 "FPS overlay toggled: {}",
851 if self.debug.show_fps_overlay {
852 "visible"
853 } else {
854 "hidden"
855 }
856 );
857
858 if let Some(window) = &self.window {
860 window.request_redraw();
861 }
862
863 return true;
864 }
865
866 false
867 }
868
869 pub(crate) fn scroll_up_page(&mut self) {
870 let (target_offset, scrollback_len) = {
872 let tab = if let Some(t) = self.tab_manager.active_tab() {
873 t
874 } else {
875 return;
876 };
877 (tab.scroll_state.target_offset, tab.cache.scrollback_len)
878 };
879
880 if let Some(renderer) = &self.renderer {
881 let char_height = self.config.font_size * 1.2;
882 let page_size = (renderer.size().height as f32 / char_height) as usize;
883
884 let new_target = target_offset.saturating_add(page_size);
885 let clamped_target = new_target.min(scrollback_len);
886 self.set_scroll_target(clamped_target);
887 }
888 }
889
890 pub(crate) fn scroll_down_page(&mut self) {
891 let target_offset = {
893 if let Some(tab) = self.tab_manager.active_tab() {
894 tab.scroll_state.target_offset
895 } else {
896 return;
897 }
898 };
899
900 if let Some(renderer) = &self.renderer {
901 let char_height = self.config.font_size * 1.2;
902 let page_size = (renderer.size().height as f32 / char_height) as usize;
903
904 let new_target = target_offset.saturating_sub(page_size);
905 self.set_scroll_target(new_target);
906 }
907 }
908
909 pub(crate) fn scroll_to_top(&mut self) {
910 let scrollback_len = {
911 if let Some(tab) = self.tab_manager.active_tab() {
912 tab.cache.scrollback_len
913 } else {
914 return;
915 }
916 };
917 self.set_scroll_target(scrollback_len);
918 }
919
920 pub(crate) fn scroll_to_bottom(&mut self) {
921 self.set_scroll_target(0);
922 }
923
924 pub(crate) fn is_egui_using_pointer(&self) -> bool {
926 let any_ui_visible =
928 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
929 if !any_ui_visible {
930 return false;
931 }
932
933 if let Some(ctx) = &self.egui_ctx {
935 ctx.is_using_pointer() || ctx.wants_pointer_input()
936 } else {
937 false
938 }
939 }
940
941 pub(crate) fn is_egui_using_keyboard(&self) -> bool {
943 let any_ui_visible =
945 self.settings_ui.visible || self.help_ui.visible || self.clipboard_history_ui.visible;
946 if !any_ui_visible {
947 return false;
948 }
949
950 if let Some(ctx) = &self.egui_ctx {
952 ctx.wants_keyboard_input()
953 } else {
954 false
955 }
956 }
957
958 pub(crate) fn should_show_scrollbar(&self) -> bool {
960 let tab = match self.tab_manager.active_tab() {
961 Some(t) => t,
962 None => return false,
963 };
964
965 if tab.cache.scrollback_len == 0 {
967 return false;
968 }
969
970 if tab.scroll_state.dragging {
972 return true;
973 }
974
975 if self.config.scrollbar_autohide_delay == 0 {
977 return true;
978 }
979
980 if tab.scroll_state.offset > 0 || tab.scroll_state.target_offset > 0 {
982 return true;
983 }
984
985 if let Some(window) = &self.window {
987 let padding = 32.0; let width = window.inner_size().width as f64;
989 let near_right = self.config.scrollbar_position != "left"
990 && (width - tab.mouse.position.0) <= padding;
991 let near_left =
992 self.config.scrollbar_position == "left" && tab.mouse.position.0 <= padding;
993 if near_left || near_right {
994 return true;
995 }
996 }
997
998 tab.scroll_state.last_activity.elapsed().as_millis()
1000 < self.config.scrollbar_autohide_delay as u128
1001 }
1002
1003 pub(crate) fn update_cursor_blink(&mut self) {
1005 if !self.config.cursor_blink {
1006 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1008 return;
1009 }
1010
1011 let now = std::time::Instant::now();
1012
1013 if let Some(last_key) = self.last_key_press
1015 && now.duration_since(last_key).as_millis() < 500
1016 {
1017 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1018 self.last_cursor_blink = Some(now);
1019 return;
1020 }
1021
1022 let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
1024
1025 if let Some(last_blink) = self.last_cursor_blink {
1026 let elapsed = now.duration_since(last_blink);
1027 let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
1028
1029 self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
1031 .abs()
1032 .clamp(0.0, 1.0);
1033
1034 if elapsed >= blink_interval * 2 {
1036 self.last_cursor_blink = Some(now);
1037 }
1038 } else {
1039 self.cursor_opacity = 1.0;
1041 self.last_cursor_blink = Some(now);
1042 }
1043 }
1044
1045 pub(crate) fn render(&mut self) {
1047 if self.is_shutting_down {
1049 return;
1050 }
1051
1052 let absolute_start = std::time::Instant::now();
1053
1054 self.needs_redraw = false;
1057
1058 let frame_start = std::time::Instant::now();
1060
1061 if let Some(last_start) = self.debug.last_frame_start {
1063 let frame_time = frame_start.duration_since(last_start);
1064 self.debug.frame_times.push(frame_time);
1065 if self.debug.frame_times.len() > 60 {
1066 self.debug.frame_times.remove(0);
1067 }
1068 }
1069 self.debug.last_frame_start = Some(frame_start);
1070
1071 let animation_running = if let Some(tab) = self.tab_manager.active_tab_mut() {
1073 tab.scroll_state.update_animation()
1074 } else {
1075 false
1076 };
1077
1078 self.tab_manager.update_all_titles();
1080
1081 if self.pending_font_rebuild {
1083 if let Err(e) = self.rebuild_renderer() {
1084 log::error!("Failed to rebuild renderer after font change: {}", e);
1085 }
1086 self.pending_font_rebuild = false;
1087 }
1088
1089 let (renderer_size, visible_lines) = if let Some(renderer) = &self.renderer {
1090 (renderer.size(), renderer.grid_size().1)
1091 } else {
1092 return;
1093 };
1094
1095 let tab = match self.tab_manager.active_tab() {
1097 Some(t) => t,
1098 None => return,
1099 };
1100 let terminal = &tab.terminal;
1101
1102 let _is_running = if let Ok(term) = terminal.try_lock() {
1104 term.is_running()
1105 } else {
1106 true };
1108
1109 if animation_running && let Some(window) = &self.window {
1111 window.request_redraw();
1112 }
1113
1114 let scroll_offset = tab.scroll_state.offset;
1116 let mouse_selection = tab.mouse.selection;
1117
1118 let (cells, current_cursor_pos, cursor_style) = if let Ok(term) = terminal.try_lock() {
1120 let current_generation = term.update_generation();
1122
1123 let (selection, rectangular) = if let Some(sel) = mouse_selection {
1125 (
1126 Some(sel.normalized()),
1127 sel.mode == SelectionMode::Rectangular,
1128 )
1129 } else {
1130 (None, false)
1131 };
1132
1133 let current_cursor_pos = if scroll_offset == 0 && term.is_cursor_visible() {
1136 Some(term.cursor_position())
1137 } else {
1138 None
1139 };
1140
1141 let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
1142
1143 let cursor_style = if current_cursor_pos.is_some() {
1145 Some(term.cursor_style())
1146 } else {
1147 None
1148 };
1149
1150 log::trace!(
1151 "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
1152 current_cursor_pos,
1153 self.cursor_opacity,
1154 cursor_style,
1155 scroll_offset,
1156 term.is_cursor_visible()
1157 );
1158
1159 let needs_regeneration = tab.cache.cells.is_none()
1162 || current_generation != tab.cache.generation
1163 || scroll_offset != tab.cache.scroll_offset
1164 || current_cursor_pos != tab.cache.cursor_pos || mouse_selection != tab.cache.selection; let cell_gen_start = std::time::Instant::now();
1168 let (cells, is_cache_hit) = if needs_regeneration {
1169 let fresh_cells =
1171 term.get_cells_with_scrollback(scroll_offset, selection, rectangular, cursor);
1172
1173 (fresh_cells, false)
1174 } else {
1175 (tab.cache.cells.as_ref().unwrap().clone(), true)
1178 };
1179 self.debug.cache_hit = is_cache_hit;
1180 self.debug.cell_gen_time = cell_gen_start.elapsed();
1181
1182 (cells, current_cursor_pos, cursor_style)
1183 } else {
1184 return; };
1186
1187 if !self.debug.cache_hit
1190 && let Some(tab) = self.tab_manager.active_tab_mut()
1191 && let Ok(term) = tab.terminal.try_lock()
1192 {
1193 let current_generation = term.update_generation();
1194 tab.cache.cells = Some(cells.clone());
1195 tab.cache.generation = current_generation;
1196 tab.cache.scroll_offset = tab.scroll_state.offset;
1197 tab.cache.cursor_pos = current_cursor_pos;
1198 tab.cache.selection = tab.mouse.selection;
1199 }
1200
1201 let tab = match self.tab_manager.active_tab() {
1206 Some(t) => t,
1207 None => return,
1208 };
1209 let terminal = &tab.terminal;
1210 let cached_scrollback_len = tab.cache.scrollback_len;
1211 let cached_terminal_title = tab.cache.terminal_title.clone();
1212 let hovered_url = tab.mouse.hovered_url.clone();
1213
1214 let (scrollback_len, terminal_title) = if let Ok(term) = terminal.try_lock() {
1215 (term.scrollback_len(), term.get_title())
1216 } else {
1217 (cached_scrollback_len, cached_terminal_title.clone())
1218 };
1219
1220 if let Some(tab) = self.tab_manager.active_tab_mut() {
1222 tab.cache.scrollback_len = scrollback_len;
1223 tab.scroll_state
1224 .clamp_to_scrollback(tab.cache.scrollback_len);
1225 }
1226
1227 if self.config.allow_title_change
1230 && hovered_url.is_none()
1231 && terminal_title != cached_terminal_title
1232 {
1233 if let Some(tab) = self.tab_manager.active_tab_mut() {
1234 tab.cache.terminal_title = terminal_title.clone();
1235 }
1236 if let Some(window) = &self.window {
1237 if terminal_title.is_empty() {
1238 window.set_title(&self.config.window_title);
1240 } else {
1241 window.set_title(&terminal_title);
1243 }
1244 }
1245 }
1246
1247 let total_lines = visible_lines + scrollback_len;
1249
1250 let url_detect_start = std::time::Instant::now();
1253 let debug_url_detect_time = if !self.debug.cache_hit {
1254 self.detect_urls();
1256 url_detect_start.elapsed()
1257 } else {
1258 std::time::Duration::ZERO
1260 };
1261
1262 let url_underline_start = std::time::Instant::now();
1264 let mut cells = cells; self.apply_url_underlines(&mut cells, &renderer_size);
1266 let _debug_url_underline_time = url_underline_start.elapsed();
1267
1268 self.update_cursor_blink();
1270
1271 let render_start = std::time::Instant::now();
1272
1273 let mut debug_update_cells_time = std::time::Duration::ZERO;
1274 #[allow(unused_assignments)]
1275 let mut debug_graphics_time = std::time::Duration::ZERO;
1276 #[allow(unused_assignments)]
1277 let mut debug_actual_render_time = std::time::Duration::ZERO;
1278 let _ = &debug_actual_render_time;
1279 let mut pending_clipboard_action = ClipboardHistoryAction::None;
1281 let mut pending_tab_action = TabBarAction::None;
1283
1284 let show_scrollbar = self.should_show_scrollbar();
1285
1286 if let Some(renderer) = &mut self.renderer {
1287 if !self.debug.cache_hit {
1290 let t = std::time::Instant::now();
1291 renderer.update_cells(&cells);
1292 debug_update_cells_time = t.elapsed();
1293 }
1294
1295 if let (Some(pos), Some(opacity), Some(style)) =
1297 (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
1298 {
1299 renderer.update_cursor(pos, opacity, style);
1300 let cursor_color = [
1303 self.config.cursor_color[0] as f32 / 255.0,
1304 self.config.cursor_color[1] as f32 / 255.0,
1305 self.config.cursor_color[2] as f32 / 255.0,
1306 1.0,
1307 ];
1308 renderer.update_shader_cursor(pos.0, pos.1, opacity, cursor_color, style);
1309 } else {
1310 renderer.clear_cursor();
1311 }
1312
1313 if self.settings_ui.visible {
1315 let ui_cfg = self.settings_ui.current_config().clone();
1316 if (ui_cfg.window_opacity - self.config.window_opacity).abs() > 1e-4 {
1317 log::info!(
1318 "Syncing live opacity from UI {:.3} (app {:.3})",
1319 ui_cfg.window_opacity,
1320 self.config.window_opacity
1321 );
1322 self.config.window_opacity = ui_cfg.window_opacity;
1323 }
1324
1325 renderer.update_opacity(self.config.window_opacity);
1326 if let Some(tab) = self.tab_manager.active_tab_mut() {
1327 tab.cache.applied_opacity = self.config.window_opacity;
1328 tab.cache.cells = None;
1329 }
1330 if let Some(window) = &self.window {
1331 window.request_redraw();
1332 }
1333 }
1334
1335 let scroll_offset = self
1337 .tab_manager
1338 .active_tab()
1339 .map(|t| t.scroll_state.offset)
1340 .unwrap_or(0);
1341 renderer.update_scrollbar(scroll_offset, visible_lines, total_lines);
1342
1343 let anim_start = std::time::Instant::now();
1345 if let Some(tab) = self.tab_manager.active_tab() {
1346 let terminal = tab.terminal.blocking_lock();
1347 if terminal.update_animations() {
1348 if let Some(window) = &self.window {
1350 window.request_redraw();
1351 }
1352 }
1353 }
1354 let debug_anim_time = anim_start.elapsed();
1355
1356 let graphics_start = std::time::Instant::now();
1360 if let Some(tab) = self.tab_manager.active_tab() {
1361 let terminal = tab.terminal.blocking_lock();
1362 let mut graphics = terminal.get_graphics_with_animations();
1363 let scrollback_len = terminal.scrollback_len();
1364
1365 let scrollback_graphics = terminal.get_scrollback_graphics();
1367 let scrollback_count = scrollback_graphics.len();
1368 graphics.extend(scrollback_graphics);
1369
1370 debug_info!(
1371 "APP",
1372 "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
1373 graphics.len(),
1374 scrollback_count,
1375 scroll_offset,
1376 scrollback_len
1377 );
1378 if let Err(e) = renderer.update_graphics(
1379 &graphics,
1380 scroll_offset,
1381 scrollback_len,
1382 visible_lines,
1383 ) {
1384 log::error!("Failed to update graphics: {}", e);
1385 }
1386 }
1387 debug_graphics_time = graphics_start.elapsed();
1388
1389 let visual_bell_flash = self
1391 .tab_manager
1392 .active_tab()
1393 .and_then(|t| t.bell.visual_flash);
1394 let visual_bell_intensity = if let Some(flash_start) = visual_bell_flash {
1395 const FLASH_DURATION_MS: u128 = 150;
1396 let elapsed = flash_start.elapsed().as_millis();
1397 if elapsed < FLASH_DURATION_MS {
1398 if let Some(window) = &self.window {
1400 window.request_redraw();
1401 }
1402 0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
1404 } else {
1405 if let Some(tab) = self.tab_manager.active_tab_mut() {
1407 tab.bell.visual_flash = None;
1408 }
1409 0.0
1410 }
1411 } else {
1412 0.0
1413 };
1414
1415 renderer.set_visual_bell_intensity(visual_bell_intensity);
1417
1418 let egui_start = std::time::Instant::now();
1420
1421 let show_fps = self.debug.show_fps_overlay;
1423 let fps_value = self.debug.fps_value;
1424 let frame_time_ms = if !self.debug.frame_times.is_empty() {
1425 let avg = self.debug.frame_times.iter().sum::<std::time::Duration>()
1426 / self.debug.frame_times.len() as u32;
1427 avg.as_secs_f64() * 1000.0
1428 } else {
1429 0.0
1430 };
1431
1432 #[allow(clippy::type_complexity)]
1434 let mut pending_config_update: Option<(
1435 Option<crate::config::Config>,
1436 Option<crate::config::Config>,
1437 Option<ShaderEditorResult>,
1438 Option<CursorShaderEditorResult>,
1439 )> = None;
1440
1441 let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
1442 (&self.egui_ctx, &mut self.egui_state)
1443 {
1444 let raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
1445
1446 let egui_output = egui_ctx.run(raw_input, |ctx| {
1447 if show_fps {
1449 egui::Area::new(egui::Id::new("fps_overlay"))
1450 .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
1451 .order(egui::Order::Foreground)
1452 .show(ctx, |ui| {
1453 egui::Frame::NONE
1454 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
1455 .inner_margin(egui::Margin::same(8))
1456 .corner_radius(4.0)
1457 .show(ui, |ui| {
1458 ui.style_mut().visuals.override_text_color =
1459 Some(egui::Color32::from_rgb(0, 255, 0));
1460 ui.label(
1461 egui::RichText::new(format!(
1462 "FPS: {:.1}\nFrame: {:.2}ms",
1463 fps_value, frame_time_ms
1464 ))
1465 .monospace()
1466 .size(14.0),
1467 );
1468 });
1469 });
1470 }
1471
1472 pending_tab_action =
1474 self.tab_bar_ui.render(ctx, &self.tab_manager, &self.config);
1475
1476 let settings_result = self.settings_ui.show(ctx);
1478 pending_config_update = Some(settings_result);
1479
1480 self.help_ui.show(ctx);
1482
1483 pending_clipboard_action = self.clipboard_history_ui.show(ctx);
1485 });
1486
1487 egui_state.handle_platform_output(
1490 self.window.as_ref().unwrap(),
1491 egui_output.platform_output.clone(),
1492 );
1493
1494 Some((egui_output, egui_ctx))
1495 } else {
1496 None
1497 };
1498
1499 if let Some((
1501 config_to_save,
1502 config_for_live_update,
1503 shader_apply,
1504 cursor_shader_apply,
1505 )) = pending_config_update
1506 {
1507 if let Some(shader_result) = shader_apply {
1509 log::info!(
1510 "Applying background shader from editor ({} bytes)",
1511 shader_result.source.len()
1512 );
1513 match renderer.reload_shader_from_source(&shader_result.source) {
1514 Ok(()) => {
1515 log::info!("Background shader applied successfully from editor");
1516 self.settings_ui.clear_shader_error();
1517 }
1518 Err(e) => {
1519 let error_msg = format!("{:#}", e);
1520 log::error!("Background shader compilation failed: {}", error_msg);
1521 self.settings_ui.set_shader_error(Some(error_msg));
1522 }
1523 }
1524 }
1525
1526 if let Some(cursor_shader_result) = cursor_shader_apply {
1528 log::info!(
1529 "Applying cursor shader from editor ({} bytes)",
1530 cursor_shader_result.source.len()
1531 );
1532 match renderer.reload_cursor_shader_from_source(&cursor_shader_result.source) {
1533 Ok(()) => {
1534 log::info!("Cursor shader applied successfully from editor");
1535 self.settings_ui.clear_cursor_shader_error();
1536 }
1537 Err(e) => {
1538 let error_msg = format!("{:#}", e);
1539 log::error!("Cursor shader compilation failed: {}", error_msg);
1540 self.settings_ui.set_cursor_shader_error(Some(error_msg));
1541 }
1542 }
1543 }
1544 if let Some(live_config) = config_for_live_update {
1546 let theme_changed = live_config.theme != self.config.theme;
1547 let shader_animation_changed =
1548 live_config.custom_shader_animation != self.config.custom_shader_animation;
1549 let shader_enabled_changed =
1550 live_config.custom_shader_enabled != self.config.custom_shader_enabled;
1551 let shader_path_changed =
1552 live_config.custom_shader != self.config.custom_shader;
1553 let shader_speed_changed = (live_config.custom_shader_animation_speed
1554 - self.config.custom_shader_animation_speed)
1555 .abs()
1556 > f32::EPSILON;
1557 let shader_full_content_changed = live_config.custom_shader_full_content
1558 != self.config.custom_shader_full_content;
1559 let shader_text_opacity_changed = (live_config.custom_shader_text_opacity
1560 - self.config.custom_shader_text_opacity)
1561 .abs()
1562 > f32::EPSILON;
1563 let shader_brightness_changed = (live_config.custom_shader_brightness
1564 - self.config.custom_shader_brightness)
1565 .abs()
1566 > f32::EPSILON;
1567 let cursor_shader_config_changed = live_config.cursor_shader_color
1568 != self.config.cursor_shader_color
1569 || (live_config.cursor_shader_trail_duration
1570 - self.config.cursor_shader_trail_duration)
1571 .abs()
1572 > f32::EPSILON
1573 || (live_config.cursor_shader_glow_radius
1574 - self.config.cursor_shader_glow_radius)
1575 .abs()
1576 > f32::EPSILON
1577 || (live_config.cursor_shader_glow_intensity
1578 - self.config.cursor_shader_glow_intensity)
1579 .abs()
1580 > f32::EPSILON;
1581 let cursor_shader_path_changed =
1582 live_config.cursor_shader != self.config.cursor_shader;
1583 let cursor_shader_enabled_changed =
1584 live_config.cursor_shader_enabled != self.config.cursor_shader_enabled;
1585 let cursor_shader_animation_changed =
1586 live_config.cursor_shader_animation != self.config.cursor_shader_animation;
1587 let cursor_shader_speed_changed = (live_config.cursor_shader_animation_speed
1588 - self.config.cursor_shader_animation_speed)
1589 .abs()
1590 > f32::EPSILON;
1591 let cursor_shader_hides_cursor_changed = live_config.cursor_shader_hides_cursor
1592 != self.config.cursor_shader_hides_cursor;
1593 let _scrollbar_position_changed =
1594 live_config.scrollbar_position != self.config.scrollbar_position;
1595 let window_title_changed = live_config.window_title != self.config.window_title;
1596 let window_decorations_changed =
1597 live_config.window_decorations != self.config.window_decorations;
1598 let max_fps_changed = live_config.max_fps != self.config.max_fps;
1599 let cursor_style_changed = live_config.cursor_style != self.config.cursor_style;
1600 let cursor_color_changed = live_config.cursor_color != self.config.cursor_color;
1601 let bg_enabled_changed = live_config.background_image_enabled
1602 != self.config.background_image_enabled;
1603 let bg_path_changed =
1604 live_config.background_image != self.config.background_image;
1605 let bg_mode_changed =
1606 live_config.background_image_mode != self.config.background_image_mode;
1607 let bg_opacity_changed = (live_config.background_image_opacity
1608 - self.config.background_image_opacity)
1609 .abs()
1610 > f32::EPSILON;
1611 let font_changed = live_config.font_family != self.config.font_family
1612 || live_config.font_family_bold != self.config.font_family_bold
1613 || live_config.font_family_italic != self.config.font_family_italic
1614 || live_config.font_family_bold_italic
1615 != self.config.font_family_bold_italic
1616 || (live_config.font_size - self.config.font_size).abs() > f32::EPSILON
1617 || (live_config.line_spacing - self.config.line_spacing).abs()
1618 > f32::EPSILON
1619 || (live_config.char_spacing - self.config.char_spacing).abs()
1620 > f32::EPSILON;
1621 let padding_changed = (live_config.window_padding - self.config.window_padding)
1622 .abs()
1623 > f32::EPSILON;
1624 log::info!(
1625 "Applying live config update - opacity: {}{}{}",
1626 live_config.window_opacity,
1627 if theme_changed {
1628 " (theme changed)"
1629 } else {
1630 ""
1631 },
1632 if font_changed { " (font changed)" } else { "" }
1633 );
1634 self.config = live_config;
1635 if let Some(tab) = self.tab_manager.active_tab_mut() {
1636 tab.scroll_state.last_activity = std::time::Instant::now();
1637 }
1638
1639 if let Some(window) = &self.window {
1641 window.set_window_level(if self.config.window_always_on_top {
1643 winit::window::WindowLevel::AlwaysOnTop
1644 } else {
1645 winit::window::WindowLevel::Normal
1646 });
1647
1648 if window_title_changed {
1650 window.set_title(&self.config.window_title);
1651 log::info!("Updated window title to: {}", self.config.window_title);
1652 }
1653
1654 if window_decorations_changed {
1656 window.set_decorations(self.config.window_decorations);
1657 log::info!(
1658 "Updated window decorations: {}",
1659 self.config.window_decorations
1660 );
1661 }
1662
1663 window.request_redraw();
1665 }
1666
1667 if max_fps_changed {
1669 if let Some(window) = &self.window {
1671 for tab in self.tab_manager.tabs_mut() {
1672 tab.stop_refresh_task();
1673 tab.start_refresh_task(
1674 Arc::clone(&self.runtime),
1675 Arc::clone(window),
1676 self.config.max_fps,
1677 );
1678 }
1679 log::info!("Updated max_fps to {} for all tabs", self.config.max_fps);
1680 }
1681 }
1682
1683 renderer.update_opacity(self.config.window_opacity);
1685 renderer.update_scrollbar_appearance(
1686 self.config.scrollbar_width,
1687 self.config.scrollbar_thumb_color,
1688 self.config.scrollbar_track_color,
1689 );
1690 if cursor_style_changed {
1693 if let Some(tab) = self.tab_manager.active_tab()
1696 && let Ok(term_mgr) = tab.terminal.try_lock()
1697 {
1698 let terminal = term_mgr.terminal();
1700 if let Some(mut term) = terminal.try_lock() {
1701 use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
1703 let term_style = match self.config.cursor_style {
1704 crate::config::CursorStyle::Block => {
1705 TermCursorStyle::SteadyBlock
1706 }
1707 crate::config::CursorStyle::Underline => {
1708 TermCursorStyle::SteadyUnderline
1709 }
1710 crate::config::CursorStyle::Beam => TermCursorStyle::SteadyBar,
1711 };
1712 term.set_cursor_style(term_style);
1713 }
1714 }
1715
1716 if let Some(tab) = self.tab_manager.active_tab_mut() {
1718 tab.cache.cells = None;
1719 tab.cache.cursor_pos = None;
1720 }
1721 if let Some(window) = &self.window {
1722 window.request_redraw();
1723 }
1724 }
1725
1726 if cursor_color_changed {
1728 renderer.update_cursor_color(self.config.cursor_color);
1729 if let Some(tab) = self.tab_manager.active_tab_mut() {
1731 tab.cache.cells = None;
1732 tab.cache.cursor_pos = None;
1733 }
1734 if let Some(window) = &self.window {
1735 window.request_redraw();
1736 }
1737 }
1738
1739 if self.config.background_image_enabled {
1740 renderer
1741 .update_background_image_opacity(self.config.background_image_opacity);
1742 }
1743
1744 if bg_enabled_changed
1745 || bg_path_changed
1746 || bg_mode_changed
1747 || bg_opacity_changed
1748 {
1749 renderer.set_background_image_enabled(
1750 self.config.background_image_enabled,
1751 self.config.background_image.as_deref(),
1752 self.config.background_image_mode,
1753 self.config.background_image_opacity,
1754 );
1755 }
1756
1757 if shader_animation_changed
1758 || shader_enabled_changed
1759 || shader_path_changed
1760 || shader_speed_changed
1761 || shader_full_content_changed
1762 || shader_text_opacity_changed
1763 || shader_brightness_changed
1764 {
1765 match renderer.set_custom_shader_enabled(
1766 self.config.custom_shader_enabled,
1767 self.config.custom_shader.as_deref(),
1768 self.config.window_opacity,
1769 self.config.custom_shader_text_opacity,
1770 self.config.custom_shader_animation,
1771 self.config.custom_shader_animation_speed,
1772 self.config.custom_shader_full_content,
1773 self.config.custom_shader_brightness,
1774 &self.config.shader_channel_paths(),
1775 ) {
1776 Ok(()) => {
1777 self.settings_ui.clear_shader_error();
1778 }
1779 Err(error_msg) => {
1780 log::error!("Shader compilation failed: {}", error_msg);
1781 self.settings_ui.set_shader_error(Some(error_msg));
1782 }
1783 }
1784 }
1785
1786 if cursor_shader_config_changed {
1788 renderer.update_cursor_shader_config(
1789 self.config.cursor_shader_color,
1790 self.config.cursor_shader_trail_duration,
1791 self.config.cursor_shader_glow_radius,
1792 self.config.cursor_shader_glow_intensity,
1793 );
1794 }
1795
1796 if cursor_shader_path_changed
1798 || cursor_shader_enabled_changed
1799 || cursor_shader_animation_changed
1800 || cursor_shader_speed_changed
1801 {
1802 match renderer.set_cursor_shader_enabled(
1803 self.config.cursor_shader_enabled,
1804 self.config.cursor_shader.as_deref(),
1805 self.config.window_opacity,
1806 self.config.cursor_shader_animation,
1807 self.config.cursor_shader_animation_speed,
1808 ) {
1809 Ok(()) => {
1810 self.settings_ui.clear_cursor_shader_error();
1811 }
1812 Err(error_msg) => {
1813 log::error!("Cursor shader compilation failed: {}", error_msg);
1814 self.settings_ui.set_cursor_shader_error(Some(error_msg));
1815 }
1816 }
1817 }
1818
1819 if cursor_shader_enabled_changed || cursor_shader_hides_cursor_changed {
1821 renderer.set_cursor_hidden_for_shader(
1822 self.config.cursor_shader_enabled
1823 && self.config.cursor_shader_hides_cursor,
1824 );
1825 }
1826
1827 if theme_changed {
1829 if let Some(tab) = self.tab_manager.active_tab()
1830 && let Ok(mut term) = tab.terminal.try_lock()
1831 {
1832 term.set_theme(self.config.load_theme());
1833 log::info!("Applied live theme change: {}", self.config.theme);
1834 }
1835 if let Some(tab) = self.tab_manager.active_tab_mut() {
1837 tab.cache.cells = None;
1838 }
1839 if let Some(window) = &self.window {
1840 window.request_redraw();
1841 }
1842 }
1843
1844 if font_changed {
1845 self.pending_font_rebuild = true;
1847 log::info!("Queued renderer rebuild for font change");
1848 }
1849
1850 if padding_changed {
1852 if let Some((cols, rows)) =
1853 renderer.update_window_padding(self.config.window_padding)
1854 {
1855 let cell_width = renderer.cell_width();
1857 let cell_height = renderer.cell_height();
1858 let width_px = (cols as f32 * cell_width) as usize;
1859 let height_px = (rows as f32 * cell_height) as usize;
1860
1861 for tab in self.tab_manager.tabs_mut() {
1862 if let Ok(mut term) = tab.terminal.try_lock() {
1863 let _ =
1864 term.resize_with_pixels(cols, rows, width_px, height_px);
1865 }
1866 }
1867 log::info!(
1868 "Resized terminals to {}x{} due to padding change",
1869 cols,
1870 rows
1871 );
1872 }
1873 log::info!("Updated window padding to {}", self.config.window_padding);
1874 }
1875
1876 if let Some(tab) = self.tab_manager.active_tab_mut() {
1878 tab.cache.cells = None;
1879 tab.cache.applied_opacity = self.config.window_opacity;
1881 }
1882
1883 if let Some(window) = &self.window {
1885 window.request_redraw();
1886 }
1887 }
1888
1889 if let Some(new_config) = config_to_save {
1891 if let Err(e) = new_config.save() {
1892 log::error!("Failed to save config: {}", e);
1893 } else {
1894 log::info!("Configuration saved successfully");
1895 log::info!(
1896 " Bell settings: sound={}, visual={}, desktop={}",
1897 new_config.notification_bell_sound,
1898 new_config.notification_bell_visual,
1899 new_config.notification_bell_desktop
1900 );
1901 self.settings_ui.update_config(new_config);
1903 }
1904 }
1905 }
1906 let debug_egui_time = egui_start.elapsed();
1907
1908 let avg_frame_time = if !self.debug.frame_times.is_empty() {
1910 self.debug.frame_times.iter().sum::<std::time::Duration>()
1911 / self.debug.frame_times.len() as u32
1912 } else {
1913 std::time::Duration::ZERO
1914 };
1915 let fps = if avg_frame_time.as_secs_f64() > 0.0 {
1916 1.0 / avg_frame_time.as_secs_f64()
1917 } else {
1918 0.0
1919 };
1920
1921 self.debug.fps_value = fps;
1923
1924 if self.debug.frame_times.len() >= 60 {
1926 let (cache_gen, cache_has_cells) = self
1927 .tab_manager
1928 .active_tab()
1929 .map(|t| (t.cache.generation, t.cache.cells.is_some()))
1930 .unwrap_or((0, false));
1931 log::info!(
1932 "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={}",
1933 fps,
1934 avg_frame_time.as_secs_f64() * 1000.0,
1935 self.debug.cell_gen_time.as_secs_f64() * 1000.0,
1936 if self.debug.cache_hit { "HIT" } else { "MISS" },
1937 debug_url_detect_time.as_secs_f64() * 1000.0,
1938 debug_anim_time.as_secs_f64() * 1000.0,
1939 debug_graphics_time.as_secs_f64() * 1000.0,
1940 debug_egui_time.as_secs_f64() * 1000.0,
1941 debug_update_cells_time.as_secs_f64() * 1000.0,
1942 debug_actual_render_time.as_secs_f64() * 1000.0,
1943 self.debug.render_time.as_secs_f64() * 1000.0,
1944 cells.len(),
1945 cache_gen,
1946 if cache_has_cells { "YES" } else { "NO" }
1947 );
1948 }
1949
1950 let actual_render_start = std::time::Instant::now();
1952 match renderer.render(egui_data, self.settings_ui.visible, show_scrollbar) {
1953 Ok(rendered) => {
1954 if !rendered {
1955 log::trace!("Skipped rendering - no changes");
1956 }
1957 }
1958 Err(e) => {
1959 if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
1962 match surface_error {
1963 SurfaceError::Outdated | SurfaceError::Lost => {
1964 log::warn!(
1965 "Surface error detected ({:?}), reconfiguring...",
1966 surface_error
1967 );
1968 self.force_surface_reconfigure();
1969 }
1970 SurfaceError::Timeout => {
1971 log::warn!("Surface timeout, will retry next frame");
1972 if let Some(window) = &self.window {
1973 window.request_redraw();
1974 }
1975 }
1976 SurfaceError::OutOfMemory => {
1977 log::error!("Surface out of memory: {:?}", surface_error);
1978 }
1979 _ => {
1980 log::error!("Surface error: {:?}", surface_error);
1981 }
1982 }
1983 } else {
1984 log::error!("Render error: {}", e);
1985 }
1986 }
1987 }
1988 debug_actual_render_time = actual_render_start.elapsed();
1989 let _ = debug_actual_render_time;
1990
1991 self.debug.render_time = render_start.elapsed();
1992 }
1993
1994 match pending_tab_action {
1997 TabBarAction::SwitchTo(id) => {
1998 self.tab_manager.switch_to(id);
1999 if let Some(renderer) = &mut self.renderer {
2001 renderer.clear_all_cells();
2002 }
2003 if let Some(tab) = self.tab_manager.active_tab_mut() {
2004 tab.cache.cells = None;
2005 }
2006 if let Some(window) = &self.window {
2007 window.request_redraw();
2008 }
2009 }
2010 TabBarAction::Close(id) => {
2011 let was_last = self.tab_manager.close_tab(id);
2012 if was_last {
2013 self.is_shutting_down = true;
2015 }
2016 if let Some(window) = &self.window {
2017 window.request_redraw();
2018 }
2019 }
2020 TabBarAction::NewTab => {
2021 self.new_tab();
2022 if let Some(window) = &self.window {
2023 window.request_redraw();
2024 }
2025 }
2026 TabBarAction::None | TabBarAction::Reorder(_, _) => {}
2027 }
2028
2029 match pending_clipboard_action {
2032 ClipboardHistoryAction::Paste(content) => {
2033 self.paste_text(&content);
2034 }
2035 ClipboardHistoryAction::ClearAll => {
2036 if let Some(tab) = self.tab_manager.active_tab()
2037 && let Ok(term) = tab.terminal.try_lock()
2038 {
2039 term.clear_all_clipboard_history();
2040 log::info!("Cleared all clipboard history");
2041 }
2042 self.clipboard_history_ui.update_entries(Vec::new());
2043 }
2044 ClipboardHistoryAction::ClearSlot(slot) => {
2045 if let Some(tab) = self.tab_manager.active_tab()
2046 && let Ok(term) = tab.terminal.try_lock()
2047 {
2048 term.clear_clipboard_history(slot);
2049 log::info!("Cleared clipboard history for slot {:?}", slot);
2050 }
2051 }
2052 ClipboardHistoryAction::None => {}
2053 }
2054
2055 let absolute_total = absolute_start.elapsed();
2056 if absolute_total.as_millis() > 10 {
2057 log::debug!(
2058 "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
2059 absolute_total.as_secs_f64() * 1000.0
2060 );
2061 }
2062 }
2063}
2064
2065impl Drop for WindowState {
2066 fn drop(&mut self) {
2067 log::info!("Shutting down window");
2068
2069 self.is_shutting_down = true;
2071
2072 let tab_count = self.tab_manager.tab_count();
2074 log::info!("Cleaning up {} tabs", tab_count);
2075
2076 for tab in self.tab_manager.tabs_mut() {
2078 tab.stop_refresh_task();
2079 }
2080 log::info!("All refresh tasks aborted");
2081
2082 std::thread::sleep(std::time::Duration::from_millis(100));
2084
2085 for tab in self.tab_manager.tabs_mut() {
2087 if let Ok(mut term) = tab.terminal.try_lock() {
2088 if term.is_running() {
2089 log::info!("Killing PTY process for tab {}", tab.id);
2090 match term.kill() {
2091 Ok(()) => {
2092 log::info!("PTY process killed successfully for tab {}", tab.id);
2093 }
2094 Err(e) => {
2095 log::warn!("Failed to kill PTY process for tab {}: {:?}", tab.id, e);
2096 }
2097 }
2098 } else {
2099 log::info!("PTY process already stopped for tab {}", tab.id);
2100 }
2101 } else {
2102 log::warn!(
2103 "Could not acquire terminal lock to kill PTY for tab {}",
2104 tab.id
2105 );
2106 }
2107 }
2108
2109 std::thread::sleep(std::time::Duration::from_millis(100));
2111
2112 log::info!("Window shutdown complete");
2113 }
2114}