1use crate::ai_inspector::chat::ChatMessage;
7use crate::ai_inspector::panel::{AIInspectorPanel, InspectorAction};
8use crate::app::anti_idle::should_send_keep_alive;
9use crate::app::debug_state::DebugState;
10use crate::badge::{BadgeState, render_badge};
11use crate::cell_renderer::PaneViewport;
12use crate::clipboard_history_ui::{ClipboardHistoryAction, ClipboardHistoryUI};
13use crate::close_confirmation_ui::{CloseConfirmAction, CloseConfirmationUI};
14use crate::command_history::CommandHistory;
15use crate::command_history_ui::{CommandHistoryAction, CommandHistoryUI};
16use crate::config::{
17 Config, CursorShaderMetadataCache, CursorStyle, ShaderInstallPrompt, ShaderMetadataCache,
18};
19use crate::help_ui::HelpUI;
20use crate::input::InputHandler;
21use crate::integrations_ui::{IntegrationsResponse, IntegrationsUI};
22use crate::keybindings::KeybindingRegistry;
23use crate::paste_special_ui::{PasteSpecialAction, PasteSpecialUI};
24use crate::profile::{ProfileManager, storage as profile_storage};
25use crate::profile_drawer_ui::{ProfileDrawerAction, ProfileDrawerUI};
26use crate::progress_bar::{ProgressBarSnapshot, render_progress_bars};
27use crate::quit_confirmation_ui::{QuitConfirmAction, QuitConfirmationUI};
28use crate::remote_shell_install_ui::{RemoteShellInstallAction, RemoteShellInstallUI};
29use crate::renderer::{
30 DividerRenderInfo, PaneDividerSettings, PaneRenderInfo, PaneTitleInfo, Renderer,
31};
32use crate::scrollback_metadata::ScrollbackMark;
33use crate::search::SearchUI;
34use crate::selection::SelectionMode;
35use crate::shader_install_ui::{ShaderInstallResponse, ShaderInstallUI};
36use crate::shader_watcher::{ShaderReloadEvent, ShaderType, ShaderWatcher};
37use crate::smart_selection::SmartSelectionCache;
38use crate::ssh_connect_ui::{SshConnectAction, SshConnectUI};
39use crate::status_bar::StatusBarUI;
40use crate::tab::{TabId, TabManager};
41use crate::tab_bar_ui::{TabBarAction, TabBarUI};
42use crate::tmux::{TmuxSession, TmuxSync};
43use crate::tmux_session_picker_ui::{SessionPickerAction, TmuxSessionPickerUI};
44use crate::tmux_status_bar_ui::TmuxStatusBarUI;
45use anyhow::Result;
46use par_term_acp::{
47 Agent, AgentConfig, AgentMessage, AgentStatus, ClientCapabilities, FsCapabilities, SafePaths,
48 discover_agents,
49};
50use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
51use std::sync::Arc;
52use tokio::runtime::Runtime;
53use tokio::sync::mpsc;
54use wgpu::SurfaceError;
55use winit::dpi::PhysicalSize;
56use winit::window::Window;
57
58struct RendererSizing {
60 size: PhysicalSize<u32>,
61 content_offset_y: f32,
62 content_offset_x: f32,
63 content_inset_bottom: f32,
64 content_inset_right: f32,
65 cell_width: f32,
66 cell_height: f32,
67 padding: f32,
68 status_bar_height: f32,
69 scale_factor: f32,
70}
71
72type PaneRenderData = (
74 PaneViewport,
75 Vec<crate::cell_renderer::Cell>,
76 (usize, usize),
77 Option<(usize, usize)>,
78 f32,
79 Vec<ScrollbackMark>,
80 usize, usize, Option<crate::pane::PaneBackground>, );
84
85pub struct WindowState {
87 pub(crate) config: Config,
88 pub(crate) window: Option<Arc<Window>>,
89 pub(crate) renderer: Option<Renderer>,
90 pub(crate) input_handler: InputHandler,
91 pub(crate) runtime: Arc<Runtime>,
92
93 pub(crate) tab_manager: TabManager,
95 pub(crate) tab_bar_ui: TabBarUI,
97 pub(crate) tmux_status_bar_ui: TmuxStatusBarUI,
99 pub(crate) status_bar_ui: StatusBarUI,
101
102 pub(crate) debug: DebugState,
103
104 pub(crate) cursor_opacity: f32,
106 pub(crate) last_cursor_blink: Option<std::time::Instant>,
108 pub(crate) last_key_press: Option<std::time::Instant>,
110 pub(crate) is_fullscreen: bool,
112 pub(crate) egui_ctx: Option<egui::Context>,
114 pub(crate) egui_state: Option<egui_winit::State>,
116 pub(crate) pending_egui_events: Vec<egui::Event>,
120 pub(crate) egui_initialized: bool,
123 pub(crate) shader_metadata_cache: ShaderMetadataCache,
125 pub(crate) cursor_shader_metadata_cache: CursorShaderMetadataCache,
127 pub(crate) help_ui: HelpUI,
129 pub(crate) clipboard_history_ui: ClipboardHistoryUI,
131 pub(crate) command_history_ui: CommandHistoryUI,
133 pub(crate) command_history: CommandHistory,
135 synced_commands: std::collections::HashSet<String>,
137 pub(crate) paste_special_ui: PasteSpecialUI,
139 pub(crate) tmux_session_picker_ui: TmuxSessionPickerUI,
141 pub(crate) search_ui: SearchUI,
143 pub(crate) ai_inspector: AIInspectorPanel,
145 pub(crate) last_inspector_width: f32,
148 pub(crate) agent_rx: Option<mpsc::UnboundedReceiver<AgentMessage>>,
150 pub(crate) agent_tx: Option<mpsc::UnboundedSender<AgentMessage>>,
152 pub(crate) agent: Option<Arc<tokio::sync::Mutex<Agent>>>,
154 pub(crate) agent_client: Option<Arc<par_term_acp::JsonRpcClient>>,
159 pub(crate) available_agents: Vec<AgentConfig>,
161 pub(crate) shader_install_ui: ShaderInstallUI,
163 pub(crate) shader_install_receiver: Option<std::sync::mpsc::Receiver<Result<usize, String>>>,
165 pub(crate) integrations_ui: IntegrationsUI,
167 pub(crate) close_confirmation_ui: CloseConfirmationUI,
169 pub(crate) quit_confirmation_ui: QuitConfirmationUI,
171 pub(crate) remote_shell_install_ui: RemoteShellInstallUI,
173 pub(crate) ssh_connect_ui: SshConnectUI,
175 pub(crate) is_recording: bool,
177 #[allow(dead_code)]
179 pub(crate) recording_start_time: Option<std::time::Instant>,
180 pub(crate) is_shutting_down: bool,
182 pub(crate) window_index: usize,
184
185 pub(crate) needs_redraw: bool,
188 pub(crate) config_changed_by_agent: bool,
191 pub(crate) cursor_blink_timer: Option<std::time::Instant>,
193 pub(crate) pending_font_rebuild: bool,
195
196 pub(crate) is_focused: bool,
199 pub(crate) last_render_time: Option<std::time::Instant>,
201
202 pub(crate) cursor_hidden_since: Option<std::time::Instant>,
205 pub(crate) flicker_pending_render: bool,
207
208 pub(crate) throughput_batch_start: Option<std::time::Instant>,
211
212 pub(crate) shader_watcher: Option<ShaderWatcher>,
215 pub(crate) config_watcher: Option<crate::config::watcher::ConfigWatcher>,
217 pub(crate) config_update_watcher: Option<crate::config::watcher::ConfigWatcher>,
219 pub(crate) shader_reload_error: Option<String>,
221 pub(crate) background_shader_reload_result: Option<Option<String>>,
224 pub(crate) cursor_shader_reload_result: Option<Option<String>>,
227
228 pub(crate) open_settings_window_requested: bool,
231
232 pub(crate) pending_arrangement_restore: Option<String>,
234
235 pub(crate) reload_dynamic_profiles_requested: bool,
237
238 pub(crate) profile_manager: ProfileManager,
241 pub(crate) profile_drawer_ui: ProfileDrawerUI,
243 pub(crate) open_settings_profiles_tab: bool,
245 pub(crate) profiles_menu_needs_update: bool,
247 pub(crate) ui_consumed_mouse_press: bool,
249 pub(crate) focus_click_pending: bool,
254
255 pub(crate) resize_overlay_visible: bool,
258 pub(crate) resize_overlay_hide_time: Option<std::time::Instant>,
260 pub(crate) resize_dimensions: Option<(u32, u32, usize, usize)>,
262
263 pub(crate) toast_message: Option<String>,
266 pub(crate) toast_hide_time: Option<std::time::Instant>,
268
269 pub(crate) pane_identify_hide_time: Option<std::time::Instant>,
272
273 pub(crate) closed_tabs: std::collections::VecDeque<super::tab_ops::ClosedTabInfo>,
275
276 pub(crate) keybinding_registry: KeybindingRegistry,
278
279 pub(crate) smart_selection_cache: SmartSelectionCache,
281
282 pub(crate) tmux_session: Option<TmuxSession>,
285 pub(crate) tmux_sync: TmuxSync,
287 pub(crate) tmux_session_name: Option<String>,
289 pub(crate) tmux_gateway_tab_id: Option<TabId>,
291 pub(crate) tmux_prefix_key: Option<crate::tmux::PrefixKey>,
293 pub(crate) tmux_prefix_state: crate::tmux::PrefixState,
295 pub(crate) tmux_pane_to_native_pane:
297 std::collections::HashMap<crate::tmux::TmuxPaneId, crate::pane::PaneId>,
298 pub(crate) native_pane_to_tmux_pane:
300 std::collections::HashMap<crate::pane::PaneId, crate::tmux::TmuxPaneId>,
301
302 pub(crate) broadcast_input: bool,
305
306 pub(crate) badge_state: BadgeState,
309
310 pub(crate) copy_mode: crate::copy_mode::CopyModeState,
313
314 pub(crate) file_transfer_state: crate::app::file_transfers::FileTransferState,
317
318 pub(crate) show_update_dialog: bool,
320
321 pub(crate) last_update_result: Option<crate::update_checker::UpdateCheckResult>,
323 pub(crate) installation_type: par_term_settings_ui::InstallationType,
325
326 pub(crate) update_installing: bool,
328 pub(crate) update_install_status: Option<String>,
330 pub(crate) update_install_receiver:
332 Option<std::sync::mpsc::Receiver<Result<crate::self_updater::UpdateResult, String>>>,
333}
334
335fn json_as_f32(value: &serde_json::Value) -> Result<f32, String> {
337 if let Some(f) = value.as_f64() {
338 Ok(f as f32)
339 } else if let Some(i) = value.as_i64() {
340 Ok(i as f32)
341 } else {
342 Err("expected number".to_string())
343 }
344}
345
346impl WindowState {
347 pub fn new(config: Config, runtime: Arc<Runtime>) -> Self {
349 let keybinding_registry = KeybindingRegistry::from_config(&config.keybindings);
350 let shaders_dir = Config::shaders_dir();
351 let tmux_prefix_key = crate::tmux::PrefixKey::parse(&config.tmux_prefix_key);
352
353 let mut input_handler = InputHandler::new();
354 input_handler
356 .update_option_key_modes(config.left_option_key_mode, config.right_option_key_mode);
357
358 let profile_manager = match profile_storage::load_profiles() {
360 Ok(manager) => manager,
361 Err(e) => {
362 log::warn!("Failed to load profiles: {}", e);
363 ProfileManager::new()
364 }
365 };
366
367 let badge_state = BadgeState::new(&config);
369 let ai_inspector = AIInspectorPanel::new(&config);
370
371 let config_dir = dirs::config_dir().unwrap_or_default().join("par-term");
373 let available_agents = discover_agents(&config_dir);
374 let command_history_max = config.command_history_max_entries;
375
376 Self {
377 config,
378 window: None,
379 renderer: None,
380 input_handler,
381 runtime,
382
383 tab_manager: TabManager::new(),
384 tab_bar_ui: TabBarUI::new(),
385 tmux_status_bar_ui: TmuxStatusBarUI::new(),
386 status_bar_ui: StatusBarUI::new(),
387
388 debug: DebugState::new(),
389
390 cursor_opacity: 1.0,
391 last_cursor_blink: None,
392 last_key_press: None,
393 is_fullscreen: false,
394 egui_ctx: None,
395 egui_state: None,
396 pending_egui_events: Vec::new(),
397 egui_initialized: false,
398 shader_metadata_cache: ShaderMetadataCache::with_shaders_dir(shaders_dir.clone()),
399 cursor_shader_metadata_cache: CursorShaderMetadataCache::with_shaders_dir(shaders_dir),
400 help_ui: HelpUI::new(),
401 clipboard_history_ui: ClipboardHistoryUI::new(),
402 command_history_ui: CommandHistoryUI::new(),
403 command_history: {
404 let mut ch = CommandHistory::new(command_history_max);
405 ch.load();
406 ch
407 },
408 synced_commands: std::collections::HashSet::new(),
409 paste_special_ui: PasteSpecialUI::new(),
410 tmux_session_picker_ui: TmuxSessionPickerUI::new(),
411 search_ui: SearchUI::new(),
412 ai_inspector,
413 last_inspector_width: 0.0,
414 agent_rx: None,
415 agent_tx: None,
416 agent: None,
417 agent_client: None,
418 available_agents,
419 shader_install_ui: ShaderInstallUI::new(),
420 shader_install_receiver: None,
421 integrations_ui: IntegrationsUI::new(),
422 close_confirmation_ui: CloseConfirmationUI::new(),
423 quit_confirmation_ui: QuitConfirmationUI::new(),
424 remote_shell_install_ui: RemoteShellInstallUI::new(),
425 ssh_connect_ui: SshConnectUI::new(),
426 is_recording: false,
427 recording_start_time: None,
428 is_shutting_down: false,
429 window_index: 1, needs_redraw: true,
432 config_changed_by_agent: false,
433 cursor_blink_timer: None,
434 pending_font_rebuild: false,
435
436 is_focused: true, last_render_time: None,
438
439 cursor_hidden_since: None,
440 flicker_pending_render: false,
441
442 throughput_batch_start: None,
443
444 shader_watcher: None,
445 config_watcher: None,
446 config_update_watcher: None,
447 shader_reload_error: None,
448 background_shader_reload_result: None,
449 cursor_shader_reload_result: None,
450
451 open_settings_window_requested: false,
452 pending_arrangement_restore: None,
453 reload_dynamic_profiles_requested: false,
454
455 profile_manager,
456 profile_drawer_ui: ProfileDrawerUI::new(),
457 open_settings_profiles_tab: false,
458 profiles_menu_needs_update: true, ui_consumed_mouse_press: false,
460 focus_click_pending: false,
461
462 resize_overlay_visible: false,
463 resize_overlay_hide_time: None,
464 resize_dimensions: None,
465
466 toast_message: None,
467 toast_hide_time: None,
468 pane_identify_hide_time: None,
469 closed_tabs: std::collections::VecDeque::new(),
470
471 keybinding_registry,
472
473 smart_selection_cache: SmartSelectionCache::new(),
474
475 tmux_session: None,
476 tmux_sync: TmuxSync::new(),
477 tmux_session_name: None,
478 tmux_gateway_tab_id: None,
479 tmux_prefix_key,
480 tmux_prefix_state: crate::tmux::PrefixState::new(),
481 tmux_pane_to_native_pane: std::collections::HashMap::new(),
482 native_pane_to_tmux_pane: std::collections::HashMap::new(),
483
484 broadcast_input: false,
485
486 badge_state,
487
488 copy_mode: crate::copy_mode::CopyModeState::new(),
489
490 file_transfer_state: crate::app::file_transfers::FileTransferState::default(),
491
492 show_update_dialog: false,
493
494 last_update_result: None,
495 installation_type: par_term_settings_ui::InstallationType::StandaloneBinary,
496
497 update_installing: false,
498 update_install_status: None,
499 update_install_receiver: None,
500 }
501 }
502
503 pub(crate) fn format_title(&self, base_title: &str) -> String {
506 if self.config.show_window_number {
507 format!("{} [{}]", base_title, self.window_index)
508 } else {
509 base_title.to_string()
510 }
511 }
512
513 #[allow(dead_code)]
517 pub(crate) fn terminal(
518 &self,
519 ) -> Option<&Arc<tokio::sync::Mutex<crate::terminal::TerminalManager>>> {
520 self.active_terminal()
521 }
522
523 #[allow(dead_code)]
524 pub(crate) fn scroll_state(&self) -> Option<&crate::scroll_state::ScrollState> {
525 self.tab_manager.active_tab().map(|t| &t.scroll_state)
526 }
527
528 #[allow(dead_code)]
529 pub(crate) fn scroll_state_mut(&mut self) -> Option<&mut crate::scroll_state::ScrollState> {
530 self.tab_manager
531 .active_tab_mut()
532 .map(|t| &mut t.scroll_state)
533 }
534
535 #[allow(dead_code)]
536 pub(crate) fn mouse(&self) -> Option<&crate::app::mouse::MouseState> {
537 self.tab_manager.active_tab().map(|t| &t.mouse)
538 }
539
540 #[allow(dead_code)]
541 pub(crate) fn mouse_mut(&mut self) -> Option<&mut crate::app::mouse::MouseState> {
542 self.tab_manager.active_tab_mut().map(|t| &mut t.mouse)
543 }
544
545 #[allow(dead_code)]
546 pub(crate) fn bell(&self) -> Option<&crate::app::bell::BellState> {
547 self.tab_manager.active_tab().map(|t| &t.bell)
548 }
549
550 #[allow(dead_code)]
551 pub(crate) fn bell_mut(&mut self) -> Option<&mut crate::app::bell::BellState> {
552 self.tab_manager.active_tab_mut().map(|t| &mut t.bell)
553 }
554
555 #[allow(dead_code)]
556 pub(crate) fn cache(&self) -> Option<&crate::app::render_cache::RenderCache> {
557 self.tab_manager.active_tab().map(|t| &t.cache)
558 }
559
560 #[allow(dead_code)]
561 pub(crate) fn cache_mut(&mut self) -> Option<&mut crate::app::render_cache::RenderCache> {
562 self.tab_manager.active_tab_mut().map(|t| &mut t.cache)
563 }
564
565 #[allow(dead_code)]
566 pub(crate) fn refresh_task(&self) -> Option<&Option<tokio::task::JoinHandle<()>>> {
567 self.tab_manager.active_tab().map(|t| &t.refresh_task)
568 }
569
570 #[allow(dead_code)]
571 pub(crate) fn abort_refresh_task(&mut self) {
572 if let Some(tab) = self.tab_manager.active_tab_mut()
573 && let Some(task) = tab.refresh_task.take()
574 {
575 task.abort();
576 }
577 }
578
579 pub(crate) fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
581 let mut extracted = String::new();
582 let end_bound = end_col.unwrap_or(usize::MAX);
583
584 if start_col > end_bound {
585 return extracted;
586 }
587
588 for (idx, ch) in line.chars().enumerate() {
589 if idx > end_bound {
590 break;
591 }
592
593 if idx >= start_col {
594 extracted.push(ch);
595 }
596 }
597
598 extracted
599 }
600
601 #[inline]
607 pub(crate) fn invalidate_tab_cache(&mut self) {
608 if let Some(tab) = self.tab_manager.active_tab_mut() {
609 tab.cache.cells = None;
610 }
611 }
612
613 #[inline]
615 pub(crate) fn request_redraw(&self) {
616 if let Some(window) = &self.window {
617 crate::debug_trace!("REDRAW", "request_redraw called");
618 window.request_redraw();
619 } else {
620 crate::debug_trace!("REDRAW", "request_redraw called but no window");
621 }
622 }
623
624 #[inline]
626 #[allow(dead_code)] pub(crate) fn invalidate_and_redraw(&mut self) {
628 self.invalidate_tab_cache();
629 self.needs_redraw = true;
630 self.request_redraw();
631 }
632
633 pub(crate) fn clear_and_invalidate(&mut self) {
635 if let Some(renderer) = &mut self.renderer {
636 renderer.clear_all_cells();
637 }
638 self.invalidate_tab_cache();
639 self.needs_redraw = true;
640 self.request_redraw();
641 }
642
643 pub(crate) fn rebuild_renderer(&mut self) -> Result<()> {
645 use crate::app::renderer_init::RendererInitParams;
646
647 let window = if let Some(w) = &self.window {
648 Arc::clone(w)
649 } else {
650 return Ok(()); };
652
653 let theme = self.config.load_theme();
655 let metadata = self
657 .config
658 .custom_shader
659 .as_ref()
660 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
661 let cursor_metadata = self
663 .config
664 .cursor_shader
665 .as_ref()
666 .and_then(|name| self.cursor_shader_metadata_cache.get(name).cloned());
667 let params = RendererInitParams::from_config(
668 &self.config,
669 &theme,
670 metadata.as_ref(),
671 cursor_metadata.as_ref(),
672 );
673
674 self.renderer = None;
678
679 let mut renderer = self
680 .runtime
681 .block_on(params.create_renderer(Arc::clone(&window)))?;
682
683 let (cols, rows) = renderer.grid_size();
684 let cell_width = renderer.cell_width();
685 let cell_height = renderer.cell_height();
686 let width_px = (cols as f32 * cell_width) as usize;
687 let height_px = (rows as f32 * cell_height) as usize;
688
689 for tab in self.tab_manager.tabs_mut() {
691 if let Ok(mut term) = tab.terminal.try_lock() {
692 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
693 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
694 term.set_theme(self.config.load_theme());
695 }
696 tab.cache.cells = None;
697 }
698
699 self.apply_cursor_shader_config(&mut renderer, ¶ms);
701
702 self.renderer = Some(renderer);
703 self.needs_redraw = true;
704
705 self.last_inspector_width = 0.0;
709 self.sync_ai_inspector_width();
710
711 self.init_egui(&window, true);
713 self.request_redraw();
714
715 Ok(())
716 }
717
718 pub(crate) async fn initialize_async(&mut self, window: Window) -> Result<()> {
720 use crate::app::renderer_init::RendererInitParams;
721
722 window.set_ime_allowed(true);
724 log::debug!("IME enabled for character input");
725
726 if self.config.auto_dark_mode {
728 let is_dark = window
729 .theme()
730 .is_none_or(|t| t == winit::window::Theme::Dark);
731 if self.config.apply_system_theme(is_dark) {
732 log::info!(
733 "Auto dark mode: detected {} system theme, using theme: {}",
734 if is_dark { "dark" } else { "light" },
735 self.config.theme
736 );
737 }
738 }
739
740 {
742 let is_dark = window
743 .theme()
744 .is_none_or(|t| t == winit::window::Theme::Dark);
745 if self.config.apply_system_tab_style(is_dark) {
746 log::info!(
747 "Auto tab style: detected {} system theme, applying {} tab style",
748 if is_dark { "dark" } else { "light" },
749 if is_dark {
750 self.config.dark_tab_style.display_name()
751 } else {
752 self.config.light_tab_style.display_name()
753 }
754 );
755 }
756 }
757
758 let window = Arc::new(window);
759
760 self.init_egui(&window, false);
762
763 let theme = self.config.load_theme();
765 let metadata = self
767 .config
768 .custom_shader
769 .as_ref()
770 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
771 let cursor_metadata = self
773 .config
774 .cursor_shader
775 .as_ref()
776 .and_then(|name| self.cursor_shader_metadata_cache.get(name).cloned());
777 let params = RendererInitParams::from_config(
778 &self.config,
779 &theme,
780 metadata.as_ref(),
781 cursor_metadata.as_ref(),
782 );
783 let mut renderer = params.create_renderer(Arc::clone(&window)).await?;
784
785 #[cfg(target_os = "macos")]
789 {
790 if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
791 log::warn!("Failed to configure Metal layer: {}", e);
792 log::warn!(
793 "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
794 );
795 }
796 if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
798 log::warn!("Failed to set initial Metal layer opacity: {}", e);
799 }
800 if self.config.blur_enabled
802 && self.config.window_opacity < 1.0
803 && let Err(e) = crate::macos_blur::set_window_blur(&window, self.config.blur_radius)
804 {
805 log::warn!("Failed to set initial window blur: {}", e);
806 }
807 }
808
809 self.apply_cursor_shader_config(&mut renderer, ¶ms);
811
812 let initial_tab_bar_height = self.tab_bar_ui.get_height(1, &self.config);
816 let initial_tab_bar_width = self.tab_bar_ui.get_width(1, &self.config);
817 let (initial_cols, initial_rows) = renderer.grid_size();
818 log::info!(
819 "Tab bar init: mode={:?}, position={:?}, height={:.1}, width={:.1}, initial_grid={}x{}, content_offset_y_before={:.1}",
820 self.config.tab_bar_mode,
821 self.config.tab_bar_position,
822 initial_tab_bar_height,
823 initial_tab_bar_width,
824 initial_cols,
825 initial_rows,
826 renderer.content_offset_y()
827 );
828 self.apply_tab_bar_offsets(&mut renderer, initial_tab_bar_height, initial_tab_bar_width);
829
830 let (renderer_cols, renderer_rows) = renderer.grid_size();
833 let cell_width = renderer.cell_width();
834 let cell_height = renderer.cell_height();
835
836 self.window = Some(Arc::clone(&window));
837 self.renderer = Some(renderer);
838
839 self.init_shader_watcher();
841
842 self.init_config_watcher();
844
845 self.init_config_update_watcher();
847
848 self.status_bar_ui.sync_monitor_state(&self.config);
850
851 log::info!(
854 "Creating first tab with grid size {}x{} (accounting for tab bar)",
855 renderer_cols,
856 renderer_rows
857 );
858 let tab_id = self.tab_manager.new_tab(
859 &self.config,
860 Arc::clone(&self.runtime),
861 false, Some((renderer_cols, renderer_rows)), )?;
864
865 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
867 let width_px = (renderer_cols as f32 * cell_width) as usize;
868 let height_px = (renderer_rows as f32 * cell_height) as usize;
869
870 if let Ok(mut term) = tab.terminal.try_lock() {
871 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
872 let _ = term.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px);
874 log::info!(
875 "Initial terminal dimensions: {}x{} ({}x{} px)",
876 renderer_cols,
877 renderer_rows,
878 width_px,
879 height_px
880 );
881 }
882
883 tab.start_refresh_task(
885 Arc::clone(&self.runtime),
886 Arc::clone(&window),
887 self.config.max_fps,
888 );
889 }
890
891 if self.ai_inspector.open {
893 self.try_auto_connect_agent();
894 }
895
896 if self.config.should_prompt_integrations() {
898 log::info!("Integrations not installed - showing welcome dialog");
899 self.integrations_ui.show_dialog();
900 self.needs_redraw = true;
901 window.request_redraw();
902 }
903
904 Ok(())
905 }
906
907 pub(crate) fn force_surface_reconfigure(&mut self) {
911 log::info!("Force surface reconfigure triggered");
912
913 if let Some(renderer) = &mut self.renderer {
914 renderer.reconfigure_surface();
916
917 renderer.clear_glyph_cache();
919
920 if let Some(tab) = self.tab_manager.active_tab_mut() {
922 tab.cache.cells = None;
923 }
924 }
925
926 #[cfg(target_os = "macos")]
928 {
929 if let Some(window) = &self.window
930 && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
931 {
932 log::warn!("Failed to reconfigure Metal layer: {}", e);
933 }
934 }
935
936 self.needs_redraw = true;
938 self.request_redraw();
939 }
940
941 pub(crate) fn apply_tab_bar_offsets(
949 &self,
950 renderer: &mut crate::renderer::Renderer,
951 tab_bar_height: f32,
952 tab_bar_width: f32,
953 ) -> Option<(usize, usize)> {
954 Self::apply_tab_bar_offsets_for_position(
955 self.config.tab_bar_position,
956 renderer,
957 tab_bar_height,
958 tab_bar_width,
959 )
960 }
961
962 pub(crate) fn apply_tab_bar_offsets_for_position(
964 position: crate::config::TabBarPosition,
965 renderer: &mut crate::renderer::Renderer,
966 tab_bar_height: f32,
967 tab_bar_width: f32,
968 ) -> Option<(usize, usize)> {
969 use crate::config::TabBarPosition;
970 let (offset_y, offset_x, inset_bottom) = match position {
971 TabBarPosition::Top => (tab_bar_height, 0.0, 0.0),
972 TabBarPosition::Bottom => (0.0, 0.0, tab_bar_height),
973 TabBarPosition::Left => (0.0, tab_bar_width, 0.0),
974 };
975
976 let mut result = None;
977 if let Some(grid) = renderer.set_content_offset_y(offset_y) {
978 result = Some(grid);
979 }
980 if let Some(grid) = renderer.set_content_offset_x(offset_x) {
981 result = Some(grid);
982 }
983 if let Some(grid) = renderer.set_content_inset_bottom(inset_bottom) {
984 result = Some(grid);
985 }
986 result
987 }
988
989 pub(crate) fn sync_ai_inspector_width(&mut self) {
999 let current_width = self.ai_inspector.consumed_width();
1000
1001 if let Some(renderer) = &mut self.renderer {
1002 if let Some((new_cols, new_rows)) = renderer.set_content_inset_right(current_width) {
1009 let cell_width = renderer.cell_width();
1010 let cell_height = renderer.cell_height();
1011 let width_px = (new_cols as f32 * cell_width) as usize;
1012 let height_px = (new_rows as f32 * cell_height) as usize;
1013
1014 for tab in self.tab_manager.tabs_mut() {
1015 if let Ok(mut term) = tab.terminal.try_lock() {
1016 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
1017 let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
1018 }
1019 tab.cache.cells = None;
1020 }
1021
1022 crate::debug_info!(
1023 "AI_INSPECTOR",
1024 "Panel width synced to {:.0}px, resized terminals to {}x{}",
1025 current_width,
1026 new_cols,
1027 new_rows
1028 );
1029 self.needs_redraw = true;
1030 } else if (current_width - self.last_inspector_width).abs() >= 1.0 {
1031 self.needs_redraw = true;
1034 }
1035 }
1036
1037 if !self.ai_inspector.is_resizing()
1039 && (current_width - self.last_inspector_width).abs() >= 1.0
1040 && current_width > 0.0
1041 && self.ai_inspector.open
1042 {
1043 self.config.ai_inspector_width = self.ai_inspector.width;
1044 if let Err(e) = self.config.save() {
1046 log::error!("Failed to save AI inspector width: {}", e);
1047 }
1048 }
1049
1050 self.last_inspector_width = current_width;
1051 }
1052
1053 pub(crate) fn connect_agent(&mut self, identity: &str) {
1058 if let Some(agent_config) = self
1059 .available_agents
1060 .iter()
1061 .find(|a| a.identity == identity)
1062 {
1063 if let Some(old_agent) = self.agent.take() {
1065 let runtime = self.runtime.clone();
1066 runtime.spawn(async move {
1067 let mut agent = old_agent.lock().await;
1068 agent.disconnect().await;
1069 });
1070 }
1071 self.agent_rx = None;
1072 self.agent_tx = None;
1073 self.agent_client = None;
1074
1075 let (tx, rx) = mpsc::unbounded_channel();
1076 self.agent_rx = Some(rx);
1077 self.agent_tx = Some(tx.clone());
1078 let ui_tx = tx.clone();
1079 let safe_paths = SafePaths {
1080 config_dir: Config::config_dir(),
1081 shaders_dir: Config::shaders_dir(),
1082 };
1083 let mcp_server_bin =
1084 std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("par-term"));
1085 let agent = Agent::new(agent_config.clone(), tx, safe_paths, mcp_server_bin);
1086 agent.auto_approve.store(
1087 self.config.ai_inspector_auto_approve,
1088 std::sync::atomic::Ordering::Relaxed,
1089 );
1090 let agent = Arc::new(tokio::sync::Mutex::new(agent));
1091 self.agent = Some(agent.clone());
1092
1093 let fallback_cwd = std::env::current_dir()
1095 .unwrap_or_default()
1096 .to_string_lossy()
1097 .to_string();
1098 let cwd = if let Some(tab) = self.tab_manager.active_tab() {
1099 if let Ok(term) = tab.terminal.try_lock() {
1100 term.shell_integration_cwd()
1101 .unwrap_or_else(|| fallback_cwd.clone())
1102 } else {
1103 fallback_cwd.clone()
1104 }
1105 } else {
1106 fallback_cwd
1107 };
1108
1109 let capabilities = ClientCapabilities {
1110 fs: FsCapabilities {
1111 read_text_file: true,
1112 write_text_file: true,
1113 list_directory: true,
1114 find: true,
1115 },
1116 terminal: self.config.ai_inspector_agent_terminal_access,
1117 config: true,
1118 };
1119
1120 let auto_approve = self.config.ai_inspector_auto_approve;
1121 let runtime = self.runtime.clone();
1122 runtime.spawn(async move {
1123 let mut agent = agent.lock().await;
1124 if let Err(e) = agent.connect(&cwd, capabilities).await {
1125 log::error!("ACP: failed to connect to agent: {e}");
1126 return;
1127 }
1128 if let Some(client) = &agent.client {
1129 let _ = ui_tx.send(AgentMessage::ClientReady(Arc::clone(client)));
1130 }
1131 if auto_approve && let Err(e) = agent.set_mode("bypassPermissions").await {
1132 log::error!("ACP: failed to set bypassPermissions mode: {e}");
1133 }
1134 });
1135 }
1136 }
1137
1138 pub(crate) fn try_auto_connect_agent(&mut self) {
1140 if self.config.ai_inspector_auto_launch
1141 && self.ai_inspector.agent_status == AgentStatus::Disconnected
1142 && self.agent.is_none()
1143 {
1144 let identity = self.config.ai_inspector_agent.clone();
1145 if !identity.is_empty() {
1146 log::info!("ACP: auto-connecting to agent '{}'", identity);
1147 self.connect_agent(&identity);
1148 }
1149 }
1150 }
1151
1152 pub(crate) fn sync_status_bar_inset(&mut self) {
1162 let is_tmux = self.is_tmux_connected();
1163 let tmux_bar = crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux);
1164 let custom_bar = self.status_bar_ui.height(&self.config, self.is_fullscreen);
1165 let total = tmux_bar + custom_bar;
1166
1167 if let Some(renderer) = &mut self.renderer
1168 && let Some((new_cols, new_rows)) = renderer.set_egui_bottom_inset(total)
1169 {
1170 let cell_width = renderer.cell_width();
1171 let cell_height = renderer.cell_height();
1172 let width_px = (new_cols as f32 * cell_width) as usize;
1173 let height_px = (new_rows as f32 * cell_height) as usize;
1174
1175 for tab in self.tab_manager.tabs_mut() {
1176 if let Ok(mut term) = tab.terminal.try_lock() {
1177 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
1178 let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
1179 }
1180 tab.cache.cells = None;
1181 }
1182 }
1183 }
1184
1185 pub(crate) fn init_shader_watcher(&mut self) {
1190 debug_info!(
1191 "SHADER",
1192 "init_shader_watcher: hot_reload={}",
1193 self.config.shader_hot_reload
1194 );
1195
1196 if !self.config.shader_hot_reload {
1197 log::debug!("Shader hot reload disabled");
1198 return;
1199 }
1200
1201 let background_path = self
1202 .config
1203 .custom_shader
1204 .as_ref()
1205 .filter(|_| self.config.custom_shader_enabled)
1206 .map(|s| Config::shader_path(s));
1207
1208 let cursor_path = self
1209 .config
1210 .cursor_shader
1211 .as_ref()
1212 .filter(|_| self.config.cursor_shader_enabled)
1213 .map(|s| Config::shader_path(s));
1214
1215 debug_info!(
1216 "SHADER",
1217 "Shader paths: background={:?}, cursor={:?}",
1218 background_path,
1219 cursor_path
1220 );
1221
1222 if background_path.is_none() && cursor_path.is_none() {
1223 debug_info!("SHADER", "No shaders to watch for hot reload");
1224 return;
1225 }
1226
1227 match ShaderWatcher::new(
1228 background_path.as_deref(),
1229 cursor_path.as_deref(),
1230 self.config.shader_hot_reload_delay,
1231 ) {
1232 Ok(watcher) => {
1233 debug_info!(
1234 "SHADER",
1235 "Shader hot reload initialized (debounce: {}ms)",
1236 self.config.shader_hot_reload_delay
1237 );
1238 self.shader_watcher = Some(watcher);
1239 }
1240 Err(e) => {
1241 debug_info!("SHADER", "Failed to initialize shader hot reload: {}", e);
1242 }
1243 }
1244 }
1245
1246 pub(crate) fn reinit_shader_watcher(&mut self) {
1248 debug_info!(
1249 "SHADER",
1250 "reinit_shader_watcher CALLED: shader={:?}, cursor={:?}",
1251 self.config.custom_shader,
1252 self.config.cursor_shader
1253 );
1254 self.shader_watcher = None;
1256 self.shader_reload_error = None;
1257
1258 self.init_shader_watcher();
1260 }
1261
1262 pub(crate) fn init_config_watcher(&mut self) {
1267 let config_path = Config::config_path();
1268 if !config_path.exists() {
1269 debug_info!("CONFIG", "Config file does not exist, skipping watcher");
1270 return;
1271 }
1272 match crate::config::watcher::ConfigWatcher::new(&config_path, 500) {
1273 Ok(watcher) => {
1274 debug_info!("CONFIG", "Config watcher initialized");
1275 self.config_watcher = Some(watcher);
1276 }
1277 Err(e) => {
1278 debug_info!("CONFIG", "Failed to initialize config watcher: {}", e);
1279 }
1280 }
1281 }
1282
1283 pub(crate) fn init_config_update_watcher(&mut self) {
1288 let update_path = Config::config_dir().join(".config-update.json");
1289
1290 if !update_path.exists() {
1292 if let Some(parent) = update_path.parent() {
1293 let _ = std::fs::create_dir_all(parent);
1294 }
1295 let _ = std::fs::write(&update_path, "");
1296 }
1297
1298 match crate::config::watcher::ConfigWatcher::new(&update_path, 200) {
1299 Ok(watcher) => {
1300 debug_info!("CONFIG", "Config-update watcher initialized");
1301 self.config_update_watcher = Some(watcher);
1302 }
1303 Err(e) => {
1304 debug_info!(
1305 "CONFIG",
1306 "Failed to initialize config-update watcher: {}",
1307 e
1308 );
1309 }
1310 }
1311 }
1312
1313 pub(crate) fn check_config_update_file(&mut self) {
1318 let Some(watcher) = &self.config_update_watcher else {
1319 return;
1320 };
1321 if watcher.try_recv().is_none() {
1322 return;
1323 }
1324
1325 let update_path = Config::config_dir().join(".config-update.json");
1326 let content = match std::fs::read_to_string(&update_path) {
1327 Ok(c) if c.trim().is_empty() => return,
1328 Ok(c) => c,
1329 Err(e) => {
1330 log::warn!("CONFIG: failed to read config-update file: {e}");
1331 return;
1332 }
1333 };
1334
1335 match serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(&content)
1336 {
1337 Ok(updates) => {
1338 log::info!(
1339 "CONFIG: applying MCP config update ({} keys): {:?}",
1340 updates.len(),
1341 updates
1342 );
1343 if let Err(e) = self.apply_agent_config_updates(&updates) {
1344 log::error!("CONFIG: MCP config update failed: {e}");
1345 } else {
1346 self.config_changed_by_agent = true;
1347 }
1348 self.needs_redraw = true;
1349 }
1350 Err(e) => {
1351 log::error!("CONFIG: invalid JSON in config-update file: {e}");
1352 }
1353 }
1354
1355 let _ = std::fs::write(&update_path, "");
1357 }
1358
1359 pub(crate) fn check_config_reload(&mut self) {
1366 let Some(watcher) = &self.config_watcher else {
1367 return;
1368 };
1369 let Some(_event) = watcher.try_recv() else {
1370 return;
1371 };
1372
1373 log::info!("CONFIG: config file changed, reloading...");
1374
1375 match Config::load() {
1376 Ok(new_config) => {
1377 use crate::app::config_updates::ConfigChanges;
1378
1379 let changes = ConfigChanges::detect(&self.config, &new_config);
1380
1381 self.config = new_config;
1384
1385 log::info!(
1386 "CONFIG: shader_changed={} cursor_changed={} shader={:?}",
1387 changes.any_shader_change(),
1388 changes.any_cursor_shader_toggle(),
1389 self.config.custom_shader
1390 );
1391
1392 if let Some(renderer) = &mut self.renderer {
1394 if changes.any_shader_change() || changes.shader_per_shader_config {
1395 log::info!("CONFIG: applying background shader change to renderer");
1396 let shader_override = self
1397 .config
1398 .custom_shader
1399 .as_ref()
1400 .and_then(|name| self.config.shader_configs.get(name));
1401 let metadata = self
1402 .config
1403 .custom_shader
1404 .as_ref()
1405 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
1406 let resolved = crate::config::shader_config::resolve_shader_config(
1407 shader_override,
1408 metadata.as_ref(),
1409 &self.config,
1410 );
1411 if let Err(e) = renderer.set_custom_shader_enabled(
1412 self.config.custom_shader_enabled,
1413 self.config.custom_shader.as_deref(),
1414 self.config.window_opacity,
1415 self.config.custom_shader_animation,
1416 resolved.animation_speed,
1417 resolved.full_content,
1418 resolved.brightness,
1419 &resolved.channel_paths(),
1420 resolved.cubemap_path().map(|p| p.as_path()),
1421 ) {
1422 log::error!("Config reload: shader load failed: {e}");
1423 }
1424 }
1425 if changes.any_cursor_shader_toggle() {
1426 log::info!("CONFIG: applying cursor shader change to renderer");
1427 if let Err(e) = renderer.set_cursor_shader_enabled(
1428 self.config.cursor_shader_enabled,
1429 self.config.cursor_shader.as_deref(),
1430 self.config.window_opacity,
1431 self.config.cursor_shader_animation,
1432 self.config.cursor_shader_animation_speed,
1433 ) {
1434 log::error!("Config reload: cursor shader load failed: {e}");
1435 }
1436 }
1437 }
1438
1439 if changes.needs_watcher_reinit() {
1441 self.reinit_shader_watcher();
1442 }
1443
1444 self.needs_redraw = true;
1445 debug_info!("CONFIG", "Config reloaded successfully");
1446 }
1447 Err(e) => {
1448 log::error!("Failed to reload config: {}", e);
1449 }
1450 }
1451 }
1452
1453 fn apply_agent_config_updates(
1458 &mut self,
1459 updates: &std::collections::HashMap<String, serde_json::Value>,
1460 ) -> Result<(), String> {
1461 let mut errors = Vec::new();
1462 let old_config = self.config.clone();
1463
1464 for (key, value) in updates {
1465 if let Err(e) = self.apply_single_config_update(key, value) {
1466 errors.push(format!("{key}: {e}"));
1467 }
1468 }
1469
1470 if !errors.is_empty() {
1471 return Err(errors.join("; "));
1472 }
1473
1474 use crate::app::config_updates::ConfigChanges;
1476 let changes = ConfigChanges::detect(&old_config, &self.config);
1477
1478 log::info!(
1479 "ACP config/update: shader_change={} cursor_change={} old_shader={:?} new_shader={:?}",
1480 changes.any_shader_change(),
1481 changes.any_cursor_shader_toggle(),
1482 old_config.custom_shader,
1483 self.config.custom_shader
1484 );
1485
1486 if let Some(renderer) = &mut self.renderer {
1487 if changes.any_shader_change() || changes.shader_per_shader_config {
1488 log::info!("ACP config/update: applying background shader change to renderer");
1489 let shader_override = self
1490 .config
1491 .custom_shader
1492 .as_ref()
1493 .and_then(|name| self.config.shader_configs.get(name));
1494 let metadata = self
1495 .config
1496 .custom_shader
1497 .as_ref()
1498 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
1499 let resolved = crate::config::shader_config::resolve_shader_config(
1500 shader_override,
1501 metadata.as_ref(),
1502 &self.config,
1503 );
1504 if let Err(e) = renderer.set_custom_shader_enabled(
1505 self.config.custom_shader_enabled,
1506 self.config.custom_shader.as_deref(),
1507 self.config.window_opacity,
1508 self.config.custom_shader_animation,
1509 resolved.animation_speed,
1510 resolved.full_content,
1511 resolved.brightness,
1512 &resolved.channel_paths(),
1513 resolved.cubemap_path().map(|p| p.as_path()),
1514 ) {
1515 log::error!("ACP config/update: shader load failed: {e}");
1516 }
1517 }
1518 if changes.any_cursor_shader_toggle() {
1519 log::info!("ACP config/update: applying cursor shader change to renderer");
1520 if let Err(e) = renderer.set_cursor_shader_enabled(
1521 self.config.cursor_shader_enabled,
1522 self.config.cursor_shader.as_deref(),
1523 self.config.window_opacity,
1524 self.config.cursor_shader_animation,
1525 self.config.cursor_shader_animation_speed,
1526 ) {
1527 log::error!("ACP config/update: cursor shader load failed: {e}");
1528 }
1529 }
1530 }
1531
1532 if changes.needs_watcher_reinit() {
1533 self.reinit_shader_watcher();
1534 }
1535
1536 if let Err(e) = self.config.save() {
1538 return Err(format!("Failed to save config: {e}"));
1539 }
1540
1541 Ok(())
1542 }
1543
1544 fn apply_single_config_update(
1546 &mut self,
1547 key: &str,
1548 value: &serde_json::Value,
1549 ) -> Result<(), String> {
1550 match key {
1551 "custom_shader" => {
1553 self.config.custom_shader = if value.is_null() {
1554 None
1555 } else {
1556 Some(value.as_str().ok_or("expected string or null")?.to_string())
1557 };
1558 Ok(())
1559 }
1560 "custom_shader_enabled" => {
1561 self.config.custom_shader_enabled = value.as_bool().ok_or("expected boolean")?;
1562 Ok(())
1563 }
1564 "custom_shader_animation" => {
1565 self.config.custom_shader_animation = value.as_bool().ok_or("expected boolean")?;
1566 Ok(())
1567 }
1568 "custom_shader_animation_speed" => {
1569 self.config.custom_shader_animation_speed = json_as_f32(value)?;
1570 Ok(())
1571 }
1572 "custom_shader_brightness" => {
1573 self.config.custom_shader_brightness = json_as_f32(value)?;
1574 Ok(())
1575 }
1576 "custom_shader_text_opacity" => {
1577 self.config.custom_shader_text_opacity = json_as_f32(value)?;
1578 Ok(())
1579 }
1580 "custom_shader_full_content" => {
1581 self.config.custom_shader_full_content =
1582 value.as_bool().ok_or("expected boolean")?;
1583 Ok(())
1584 }
1585
1586 "cursor_shader" => {
1588 self.config.cursor_shader = if value.is_null() {
1589 None
1590 } else {
1591 Some(value.as_str().ok_or("expected string or null")?.to_string())
1592 };
1593 Ok(())
1594 }
1595 "cursor_shader_enabled" => {
1596 self.config.cursor_shader_enabled = value.as_bool().ok_or("expected boolean")?;
1597 Ok(())
1598 }
1599 "cursor_shader_animation" => {
1600 self.config.cursor_shader_animation = value.as_bool().ok_or("expected boolean")?;
1601 Ok(())
1602 }
1603 "cursor_shader_animation_speed" => {
1604 self.config.cursor_shader_animation_speed = json_as_f32(value)?;
1605 Ok(())
1606 }
1607 "cursor_shader_glow_radius" => {
1608 self.config.cursor_shader_glow_radius = json_as_f32(value)?;
1609 Ok(())
1610 }
1611 "cursor_shader_glow_intensity" => {
1612 self.config.cursor_shader_glow_intensity = json_as_f32(value)?;
1613 Ok(())
1614 }
1615 "cursor_shader_trail_duration" => {
1616 self.config.cursor_shader_trail_duration = json_as_f32(value)?;
1617 Ok(())
1618 }
1619 "cursor_shader_hides_cursor" => {
1620 self.config.cursor_shader_hides_cursor =
1621 value.as_bool().ok_or("expected boolean")?;
1622 Ok(())
1623 }
1624
1625 "window_opacity" => {
1627 self.config.window_opacity = json_as_f32(value)?;
1628 Ok(())
1629 }
1630 "font_size" => {
1631 self.config.font_size = json_as_f32(value)?;
1632 Ok(())
1633 }
1634
1635 _ => Err(format!("unknown or read-only config key: {key}")),
1636 }
1637 }
1638
1639 pub(crate) fn handle_anti_idle(
1643 &mut self,
1644 now: std::time::Instant,
1645 ) -> Option<std::time::Instant> {
1646 if !self.config.anti_idle_enabled {
1647 return None;
1648 }
1649
1650 let idle_threshold = std::time::Duration::from_secs(self.config.anti_idle_seconds.max(1));
1651 let keep_alive_code = [self.config.anti_idle_code];
1652 let mut next_due: Option<std::time::Instant> = None;
1653
1654 for tab in self.tab_manager.tabs_mut() {
1655 if let Ok(term) = tab.terminal.try_lock() {
1656 let current_generation = term.update_generation();
1658 if current_generation > tab.anti_idle_last_generation {
1659 tab.anti_idle_last_generation = current_generation;
1660 tab.anti_idle_last_activity = now;
1661 }
1662
1663 if should_send_keep_alive(tab.anti_idle_last_activity, now, idle_threshold) {
1665 if let Err(e) = term.write(&keep_alive_code) {
1666 log::warn!(
1667 "Failed to send anti-idle keep-alive for tab {}: {}",
1668 tab.id,
1669 e
1670 );
1671 } else {
1672 tab.anti_idle_last_activity = now;
1673 }
1674 }
1675
1676 let elapsed = now.duration_since(tab.anti_idle_last_activity);
1678 let remaining = if elapsed >= idle_threshold {
1679 idle_threshold
1680 } else {
1681 idle_threshold - elapsed
1682 };
1683 let candidate = now + remaining;
1684 next_due = Some(next_due.map_or(candidate, |prev| prev.min(candidate)));
1685 }
1686 }
1687
1688 next_due
1689 }
1690
1691 pub(crate) fn check_shader_reload(&mut self) -> bool {
1696 let Some(watcher) = &self.shader_watcher else {
1697 return false;
1698 };
1699
1700 let Some(event) = watcher.try_recv() else {
1701 return false;
1702 };
1703
1704 self.handle_shader_reload_event(event)
1705 }
1706
1707 fn handle_shader_reload_event(&mut self, event: ShaderReloadEvent) -> bool {
1712 let shader_name = match event.shader_type {
1713 ShaderType::Background => "Background shader",
1714 ShaderType::Cursor => "Cursor shader",
1715 };
1716 let file_name = event
1717 .path
1718 .file_name()
1719 .and_then(|n| n.to_str())
1720 .unwrap_or("shader");
1721
1722 log::info!("Hot reload: {} from {}", shader_name, event.path.display());
1723
1724 let source = match std::fs::read_to_string(&event.path) {
1726 Ok(s) => s,
1727 Err(e) => {
1728 let error_msg = format!("Cannot read '{}': {}", file_name, e);
1729 log::error!("Shader hot reload failed: {}", error_msg);
1730 self.shader_reload_error = Some(error_msg.clone());
1731 match event.shader_type {
1733 ShaderType::Background => {
1734 self.background_shader_reload_result = Some(Some(error_msg.clone()));
1735 }
1736 ShaderType::Cursor => {
1737 self.cursor_shader_reload_result = Some(Some(error_msg.clone()));
1738 }
1739 }
1740 self.deliver_notification(
1742 "Shader Reload Failed",
1743 &format!("{} - {}", shader_name, error_msg),
1744 );
1745 if self.config.notification_bell_visual
1747 && let Some(tab) = self.tab_manager.active_tab_mut()
1748 {
1749 tab.bell.visual_flash = Some(std::time::Instant::now());
1750 }
1751 return false;
1752 }
1753 };
1754
1755 let Some(renderer) = &mut self.renderer else {
1756 log::error!("Cannot reload shader: no renderer available");
1757 return false;
1758 };
1759
1760 let result = match event.shader_type {
1763 ShaderType::Background => renderer.reload_shader_from_source(&source),
1764 ShaderType::Cursor => renderer.reload_cursor_shader_from_source(&source),
1765 };
1766
1767 match result {
1768 Ok(()) => {
1769 log::info!("{} reloaded successfully from {}", shader_name, file_name);
1770 self.shader_reload_error = None;
1771 match event.shader_type {
1773 ShaderType::Background => {
1774 self.background_shader_reload_result = Some(None);
1775 }
1776 ShaderType::Cursor => {
1777 self.cursor_shader_reload_result = Some(None);
1778 }
1779 }
1780 self.needs_redraw = true;
1781 self.request_redraw();
1782 true
1783 }
1784 Err(e) => {
1785 let root_cause = e.root_cause().to_string();
1787 let error_msg = if root_cause.len() > 200 {
1788 format!("{}...", &root_cause[..200])
1790 } else {
1791 root_cause
1792 };
1793
1794 log::error!(
1795 "{} compilation failed (old shader preserved): {}",
1796 shader_name,
1797 error_msg
1798 );
1799 log::debug!("Full error chain: {:#}", e);
1800
1801 self.shader_reload_error = Some(error_msg.clone());
1802 match event.shader_type {
1804 ShaderType::Background => {
1805 self.background_shader_reload_result = Some(Some(error_msg.clone()));
1806 }
1807 ShaderType::Cursor => {
1808 self.cursor_shader_reload_result = Some(Some(error_msg.clone()));
1809 }
1810 }
1811
1812 self.deliver_notification(
1814 "Shader Compilation Error",
1815 &format!("{}: {}", file_name, error_msg),
1816 );
1817
1818 if self.config.notification_bell_visual
1820 && let Some(tab) = self.tab_manager.active_tab_mut()
1821 {
1822 tab.bell.visual_flash = Some(std::time::Instant::now());
1823 }
1824
1825 false
1826 }
1827 }
1828 }
1829
1830 pub(crate) fn is_egui_using_pointer(&self) -> bool {
1832 if self.ai_inspector.wants_pointer() {
1836 return true;
1837 }
1838 if !self.egui_initialized {
1840 return false;
1841 }
1842 if let Some(ctx) = &self.egui_ctx {
1845 ctx.is_using_pointer() || ctx.wants_pointer_input()
1846 } else {
1847 false
1848 }
1849 }
1850
1851 pub(crate) fn has_egui_overlay_visible(&self) -> bool {
1855 self.search_ui.visible
1856 || self.clipboard_history_ui.visible
1857 || self.command_history_ui.visible
1858 || self.shader_install_ui.visible
1859 || self.integrations_ui.visible
1860 || self.remote_shell_install_ui.is_visible()
1861 || self.quit_confirmation_ui.is_visible()
1862 || self.ssh_connect_ui.is_visible()
1863 || self.ai_inspector.open
1864 }
1865
1866 pub(crate) fn is_egui_using_keyboard(&self) -> bool {
1868 let any_ui_visible = self.help_ui.visible
1872 || self.clipboard_history_ui.visible
1873 || self.command_history_ui.visible
1874 || self.shader_install_ui.visible
1875 || self.integrations_ui.visible
1876 || self.remote_shell_install_ui.is_visible()
1877 || self.quit_confirmation_ui.is_visible()
1878 || self.ssh_connect_ui.is_visible()
1879 || self.ai_inspector.open;
1880 if !any_ui_visible {
1881 return false;
1882 }
1883
1884 if let Some(ctx) = &self.egui_ctx {
1886 ctx.wants_keyboard_input()
1887 } else {
1888 false
1889 }
1890 }
1891
1892 pub(crate) fn should_show_scrollbar(&self) -> bool {
1894 let tab = match self.tab_manager.active_tab() {
1895 Some(t) => t,
1896 None => return false,
1897 };
1898
1899 if tab.cache.scrollback_len == 0 {
1901 return false;
1902 }
1903
1904 if tab.scroll_state.dragging {
1906 return true;
1907 }
1908
1909 if self.config.scrollbar_autohide_delay == 0 {
1911 return true;
1912 }
1913
1914 if tab.scroll_state.offset > 0 || tab.scroll_state.target_offset > 0 {
1916 return true;
1917 }
1918
1919 if let Some(window) = &self.window {
1921 let padding = 32.0; let width = window.inner_size().width as f64;
1923 let near_right = self.config.scrollbar_position != "left"
1924 && (width - tab.mouse.position.0) <= padding;
1925 let near_left =
1926 self.config.scrollbar_position == "left" && tab.mouse.position.0 <= padding;
1927 if near_left || near_right {
1928 return true;
1929 }
1930 }
1931
1932 tab.scroll_state.last_activity.elapsed().as_millis()
1934 < self.config.scrollbar_autohide_delay as u128
1935 }
1936
1937 pub(crate) fn update_cursor_blink(&mut self) {
1953 if self.config.lock_cursor_style {
1955 if !self.config.cursor_blink {
1956 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1957 return;
1958 }
1959 } else if self.config.lock_cursor_blink && !self.config.cursor_blink {
1960 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1962 return;
1963 }
1964
1965 let cursor_should_blink = if self.config.lock_cursor_style {
1967 self.config.cursor_blink
1969 } else if let Some(tab) = self.tab_manager.active_tab()
1970 && let Ok(term) = tab.terminal.try_lock()
1971 {
1972 use par_term_emu_core_rust::cursor::CursorStyle;
1973 let style = term.cursor_style();
1974 matches!(
1976 style,
1977 CursorStyle::BlinkingBlock
1978 | CursorStyle::BlinkingUnderline
1979 | CursorStyle::BlinkingBar
1980 )
1981 } else {
1982 self.config.cursor_blink
1984 };
1985
1986 if !cursor_should_blink {
1987 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1989 return;
1990 }
1991
1992 let now = std::time::Instant::now();
1993
1994 if let Some(last_key) = self.last_key_press
1996 && now.duration_since(last_key).as_millis() < 500
1997 {
1998 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1999 self.last_cursor_blink = Some(now);
2000 return;
2001 }
2002
2003 let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
2005
2006 if let Some(last_blink) = self.last_cursor_blink {
2007 let elapsed = now.duration_since(last_blink);
2008 let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
2009
2010 self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
2012 .abs()
2013 .clamp(0.0, 1.0);
2014
2015 if elapsed >= blink_interval * 2 {
2017 self.last_cursor_blink = Some(now);
2018 }
2019 } else {
2020 self.cursor_opacity = 1.0;
2022 self.last_cursor_blink = Some(now);
2023 }
2024 }
2025
2026 pub(crate) fn render(&mut self) {
2028 if self.is_shutting_down {
2030 return;
2031 }
2032
2033 let target_fps = if self.config.pause_refresh_on_blur && !self.is_focused {
2037 self.config.unfocused_fps
2038 } else {
2039 self.config.max_fps
2040 };
2041 let frame_interval_ms = 1000 / target_fps.max(1);
2042 let frame_interval = std::time::Duration::from_millis(frame_interval_ms as u64);
2043
2044 if let Some(last_render) = self.last_render_time {
2045 let elapsed = last_render.elapsed();
2046 if elapsed < frame_interval {
2047 return;
2049 }
2050 }
2051
2052 self.last_render_time = Some(std::time::Instant::now());
2054
2055 let absolute_start = std::time::Instant::now();
2056
2057 self.needs_redraw = false;
2060
2061 let frame_start = std::time::Instant::now();
2063
2064 if let Some(last_start) = self.debug.last_frame_start {
2066 let frame_time = frame_start.duration_since(last_start);
2067 self.debug.frame_times.push(frame_time);
2068 if self.debug.frame_times.len() > 60 {
2069 self.debug.frame_times.remove(0);
2070 }
2071 }
2072 self.debug.last_frame_start = Some(frame_start);
2073
2074 let animation_running = if let Some(tab) = self.tab_manager.active_tab_mut() {
2076 tab.scroll_state.update_animation()
2077 } else {
2078 false
2079 };
2080
2081 self.tab_manager.update_all_titles();
2083
2084 if self.pending_font_rebuild {
2086 if let Err(e) = self.rebuild_renderer() {
2087 log::error!("Failed to rebuild renderer after font change: {}", e);
2088 }
2089 self.pending_font_rebuild = false;
2090 }
2091
2092 let tab_count = self.tab_manager.tab_count();
2095 let tab_bar_height = self.tab_bar_ui.get_height(tab_count, &self.config);
2096 let tab_bar_width = self.tab_bar_ui.get_width(tab_count, &self.config);
2097 crate::debug_trace!(
2098 "TAB_SYNC",
2099 "Tab count={}, tab_bar_height={:.0}, tab_bar_width={:.0}, position={:?}, mode={:?}",
2100 tab_count,
2101 tab_bar_height,
2102 tab_bar_width,
2103 self.config.tab_bar_position,
2104 self.config.tab_bar_mode
2105 );
2106 if let Some(renderer) = &mut self.renderer {
2107 let grid_changed = Self::apply_tab_bar_offsets_for_position(
2108 self.config.tab_bar_position,
2109 renderer,
2110 tab_bar_height,
2111 tab_bar_width,
2112 );
2113 if let Some((new_cols, new_rows)) = grid_changed {
2114 let cell_width = renderer.cell_width();
2115 let cell_height = renderer.cell_height();
2116 let width_px = (new_cols as f32 * cell_width) as usize;
2117 let height_px = (new_rows as f32 * cell_height) as usize;
2118
2119 for tab in self.tab_manager.tabs_mut() {
2120 if let Ok(mut term) = tab.terminal.try_lock() {
2121 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
2122 let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
2123 }
2124 tab.cache.cells = None;
2125 }
2126 crate::debug_info!(
2127 "TAB_SYNC",
2128 "Tab bar offsets changed (position={:?}), resized terminals to {}x{}",
2129 self.config.tab_bar_position,
2130 new_cols,
2131 new_rows
2132 );
2133 }
2134 }
2135
2136 self.sync_status_bar_inset();
2139
2140 let (renderer_size, visible_lines, grid_cols) = if let Some(renderer) = &self.renderer {
2141 let (cols, rows) = renderer.grid_size();
2142 (renderer.size(), rows, cols)
2143 } else {
2144 return;
2145 };
2146
2147 let (
2149 terminal,
2150 scroll_offset,
2151 mouse_selection,
2152 cache_cells,
2153 cache_generation,
2154 cache_scroll_offset,
2155 cache_cursor_pos,
2156 cache_selection,
2157 cached_scrollback_len,
2158 cached_terminal_title,
2159 hovered_url,
2160 ) = match self.tab_manager.active_tab() {
2161 Some(t) => (
2162 t.terminal.clone(),
2163 t.scroll_state.offset,
2164 t.mouse.selection,
2165 t.cache.cells.clone(),
2166 t.cache.generation,
2167 t.cache.scroll_offset,
2168 t.cache.cursor_pos,
2169 t.cache.selection,
2170 t.cache.scrollback_len,
2171 t.cache.terminal_title.clone(),
2172 t.mouse.hovered_url.clone(),
2173 ),
2174 None => return,
2175 };
2176
2177 let _is_running = if let Ok(term) = terminal.try_lock() {
2179 term.is_running()
2180 } else {
2181 true };
2183
2184 if animation_running && let Some(window) = &self.window {
2186 window.request_redraw();
2187 }
2188
2189 let (cells, current_cursor_pos, cursor_style, is_alt_screen) = if let Ok(term) =
2194 terminal.try_lock()
2195 {
2196 let current_generation = term.update_generation();
2198
2199 let (selection, rectangular) = if let Some(sel) = mouse_selection {
2201 (
2202 Some(sel.normalized()),
2203 sel.mode == SelectionMode::Rectangular,
2204 )
2205 } else {
2206 (None, false)
2207 };
2208
2209 let cursor_visible = self.config.lock_cursor_visibility || term.is_cursor_visible();
2214 let current_cursor_pos = if self.copy_mode.active {
2215 self.copy_mode.screen_cursor_pos(scroll_offset)
2216 } else if scroll_offset == 0 && cursor_visible {
2217 Some(term.cursor_position())
2218 } else {
2219 None
2220 };
2221
2222 let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
2223
2224 let cursor_style = if self.copy_mode.active && current_cursor_pos.is_some() {
2229 Some(TermCursorStyle::SteadyBlock)
2230 } else if current_cursor_pos.is_some() {
2231 if self.config.lock_cursor_style {
2232 let style = if self.config.cursor_blink {
2234 match self.config.cursor_style {
2235 CursorStyle::Block => TermCursorStyle::BlinkingBlock,
2236 CursorStyle::Beam => TermCursorStyle::BlinkingBar,
2237 CursorStyle::Underline => TermCursorStyle::BlinkingUnderline,
2238 }
2239 } else {
2240 match self.config.cursor_style {
2241 CursorStyle::Block => TermCursorStyle::SteadyBlock,
2242 CursorStyle::Beam => TermCursorStyle::SteadyBar,
2243 CursorStyle::Underline => TermCursorStyle::SteadyUnderline,
2244 }
2245 };
2246 Some(style)
2247 } else {
2248 let mut style = term.cursor_style();
2249 if self.config.lock_cursor_blink && !self.config.cursor_blink {
2251 style = match style {
2252 TermCursorStyle::BlinkingBlock => TermCursorStyle::SteadyBlock,
2253 TermCursorStyle::BlinkingBar => TermCursorStyle::SteadyBar,
2254 TermCursorStyle::BlinkingUnderline => TermCursorStyle::SteadyUnderline,
2255 other => other,
2256 };
2257 }
2258 Some(style)
2259 }
2260 } else {
2261 None
2262 };
2263
2264 log::trace!(
2265 "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
2266 current_cursor_pos,
2267 self.cursor_opacity,
2268 cursor_style,
2269 scroll_offset,
2270 term.is_cursor_visible()
2271 );
2272
2273 let needs_regeneration = cache_cells.is_none()
2276 || current_generation != cache_generation
2277 || scroll_offset != cache_scroll_offset
2278 || current_cursor_pos != cache_cursor_pos || mouse_selection != cache_selection; let cell_gen_start = std::time::Instant::now();
2282 let (cells, is_cache_hit) = if needs_regeneration {
2283 let fresh_cells =
2285 term.get_cells_with_scrollback(scroll_offset, selection, rectangular, cursor);
2286
2287 (fresh_cells, false)
2288 } else {
2289 (cache_cells.as_ref().unwrap().clone(), true)
2292 };
2293 self.debug.cache_hit = is_cache_hit;
2294 self.debug.cell_gen_time = cell_gen_start.elapsed();
2295
2296 let is_alt_screen = term.is_alt_screen_active();
2298
2299 (cells, current_cursor_pos, cursor_style, is_alt_screen)
2300 } else if let Some(cached) = cache_cells {
2301 (cached, cache_cursor_pos, None, false)
2304 } else {
2305 return; };
2307
2308 let resolved_hides_cursor = self
2312 .config
2313 .cursor_shader
2314 .as_ref()
2315 .and_then(|name| self.config.cursor_shader_configs.get(name))
2316 .and_then(|override_cfg| override_cfg.hides_cursor)
2317 .or_else(|| {
2318 self.config
2319 .cursor_shader
2320 .as_ref()
2321 .and_then(|name| self.cursor_shader_metadata_cache.get(name))
2322 .and_then(|meta| meta.defaults.hides_cursor)
2323 })
2324 .unwrap_or(self.config.cursor_shader_hides_cursor);
2325 let resolved_disable_in_alt_screen = self
2327 .config
2328 .cursor_shader
2329 .as_ref()
2330 .and_then(|name| self.config.cursor_shader_configs.get(name))
2331 .and_then(|override_cfg| override_cfg.disable_in_alt_screen)
2332 .or_else(|| {
2333 self.config
2334 .cursor_shader
2335 .as_ref()
2336 .and_then(|name| self.cursor_shader_metadata_cache.get(name))
2337 .and_then(|meta| meta.defaults.disable_in_alt_screen)
2338 })
2339 .unwrap_or(self.config.cursor_shader_disable_in_alt_screen);
2340 let hide_cursor_for_shader = self.config.cursor_shader_enabled
2341 && resolved_hides_cursor
2342 && !(resolved_disable_in_alt_screen && is_alt_screen);
2343 if let Some(renderer) = &mut self.renderer {
2344 renderer.set_cursor_hidden_for_shader(hide_cursor_for_shader);
2345 }
2346
2347 if !self.debug.cache_hit
2350 && let Some(tab) = self.tab_manager.active_tab_mut()
2351 && let Ok(term) = tab.terminal.try_lock()
2352 {
2353 let current_generation = term.update_generation();
2354 tab.cache.cells = Some(cells.clone());
2355 tab.cache.generation = current_generation;
2356 tab.cache.scroll_offset = tab.scroll_state.offset;
2357 tab.cache.cursor_pos = current_cursor_pos;
2358 tab.cache.selection = tab.mouse.selection;
2359 }
2360
2361 let mut show_scrollbar = self.should_show_scrollbar();
2362
2363 let (scrollback_len, terminal_title) = if let Ok(mut term) = terminal.try_lock() {
2364 let cursor_row = current_cursor_pos.map(|(_, row)| row).unwrap_or(0);
2366 let sb_len = term.scrollback_len();
2367 term.update_scrollback_metadata(sb_len, cursor_row);
2368
2369 for mark in term.scrollback_marks() {
2376 if let Some(ref cmd) = mark.command
2377 && !cmd.is_empty()
2378 && self.synced_commands.insert(cmd.clone())
2379 {
2380 self.command_history
2381 .add(cmd.clone(), mark.exit_code, mark.duration_ms);
2382 }
2383 }
2384 for (cmd, exit_code, duration_ms) in term.core_command_history() {
2385 if !cmd.is_empty() && self.synced_commands.insert(cmd.clone()) {
2386 self.command_history.add(cmd, exit_code, duration_ms);
2387 }
2388 }
2389
2390 (sb_len, term.get_title())
2391 } else {
2392 (cached_scrollback_len, cached_terminal_title.clone())
2393 };
2394
2395 if let Some(tab) = self.tab_manager.active_tab_mut() {
2397 tab.cache.scrollback_len = scrollback_len;
2398 tab.scroll_state
2399 .clamp_to_scrollback(tab.cache.scrollback_len);
2400 }
2401
2402 if self.copy_mode.active
2404 && let Ok(term) = terminal.try_lock()
2405 {
2406 let (cols, rows) = term.dimensions();
2407 self.copy_mode.update_dimensions(cols, rows, scrollback_len);
2408 }
2409
2410 let need_marks =
2411 self.config.scrollbar_command_marks || self.config.command_separator_enabled;
2412 let mut scrollback_marks = if need_marks {
2413 if let Ok(term) = terminal.try_lock() {
2414 term.scrollback_marks()
2415 } else {
2416 Vec::new()
2417 }
2418 } else {
2419 Vec::new()
2420 };
2421
2422 if let Some(tab) = self.tab_manager.active_tab() {
2424 scrollback_marks.extend(tab.trigger_marks.iter().cloned());
2425 }
2426
2427 if !scrollback_marks.is_empty() {
2429 show_scrollbar = true;
2430 }
2431
2432 if self.config.allow_title_change
2435 && hovered_url.is_none()
2436 && terminal_title != cached_terminal_title
2437 {
2438 if let Some(tab) = self.tab_manager.active_tab_mut() {
2439 tab.cache.terminal_title = terminal_title.clone();
2440 }
2441 if let Some(window) = &self.window {
2442 if terminal_title.is_empty() {
2443 window.set_title(&self.format_title(&self.config.window_title));
2445 } else {
2446 window.set_title(&self.format_title(&terminal_title));
2448 }
2449 }
2450 }
2451
2452 let total_lines = visible_lines + scrollback_len;
2454
2455 let url_detect_start = std::time::Instant::now();
2458 let debug_url_detect_time = if !self.debug.cache_hit {
2459 self.detect_urls();
2461 url_detect_start.elapsed()
2462 } else {
2463 std::time::Duration::ZERO
2465 };
2466
2467 let url_underline_start = std::time::Instant::now();
2469 let mut cells = cells; self.apply_url_underlines(&mut cells, &renderer_size);
2471 let _debug_url_underline_time = url_underline_start.elapsed();
2472
2473 if self.search_ui.visible {
2475 if let Some(tab) = self.tab_manager.active_tab()
2477 && let Ok(term) = tab.terminal.try_lock()
2478 {
2479 let lines_iter =
2480 crate::app::search_highlight::get_all_searchable_lines(&term, visible_lines);
2481 self.search_ui.update_search(lines_iter);
2482 }
2483
2484 let scroll_offset = self
2486 .tab_manager
2487 .active_tab()
2488 .map(|t| t.scroll_state.offset)
2489 .unwrap_or(0);
2490 self.apply_search_highlights(
2492 &mut cells,
2493 grid_cols,
2494 scroll_offset,
2495 scrollback_len,
2496 visible_lines,
2497 );
2498 }
2499
2500 self.update_cursor_blink();
2502
2503 let render_start = std::time::Instant::now();
2504
2505 let mut debug_update_cells_time = std::time::Duration::ZERO;
2506 #[allow(unused_assignments)]
2507 let mut debug_graphics_time = std::time::Duration::ZERO;
2508 #[allow(unused_assignments)]
2509 let mut debug_actual_render_time = std::time::Duration::ZERO;
2510 let _ = &debug_actual_render_time;
2511 let mut pending_clipboard_action = ClipboardHistoryAction::None;
2513 let mut pending_command_history_action = CommandHistoryAction::None;
2515 let mut pending_paste_special_action = PasteSpecialAction::None;
2517 let mut pending_session_picker_action = SessionPickerAction::None;
2519 let mut pending_tab_action = TabBarAction::None;
2521 let mut pending_shader_install_response = ShaderInstallResponse::None;
2523 let mut pending_integrations_response = IntegrationsResponse::default();
2525 let mut pending_search_action = crate::search::SearchAction::None;
2527 let mut pending_inspector_action = InspectorAction::None;
2529 let mut pending_profile_drawer_action = ProfileDrawerAction::None;
2531 let mut pending_close_confirm_action = CloseConfirmAction::None;
2533 let mut pending_quit_confirm_action = QuitConfirmAction::None;
2535 let mut pending_remote_install_action = RemoteShellInstallAction::None;
2536 let mut pending_ssh_connect_action = SshConnectAction::None;
2537
2538 let msg_count_before = self.ai_inspector.chat.messages.len();
2540 type ConfigUpdateEntry = (
2543 std::collections::HashMap<String, serde_json::Value>,
2544 tokio::sync::oneshot::Sender<Result<(), String>>,
2545 );
2546 let mut pending_config_updates: Vec<ConfigUpdateEntry> = Vec::new();
2547 if let Some(rx) = &mut self.agent_rx {
2548 while let Ok(msg) = rx.try_recv() {
2549 match msg {
2550 AgentMessage::StatusChanged(status) => {
2551 self.ai_inspector.chat.flush_agent_message();
2553 self.ai_inspector.agent_status = status;
2554 self.needs_redraw = true;
2555 }
2556 AgentMessage::SessionUpdate(update) => {
2557 self.ai_inspector.chat.handle_update(update);
2558 self.needs_redraw = true;
2559 }
2560 AgentMessage::PermissionRequest {
2561 request_id,
2562 tool_call,
2563 options,
2564 } => {
2565 log::info!(
2566 "ACP: permission request id={request_id} options={}",
2567 options.len()
2568 );
2569 let description = tool_call
2570 .get("title")
2571 .and_then(|t| t.as_str())
2572 .unwrap_or("Permission requested")
2573 .to_string();
2574 self.ai_inspector
2575 .chat
2576 .messages
2577 .push(ChatMessage::Permission {
2578 request_id,
2579 description,
2580 options: options
2581 .iter()
2582 .map(|o| (o.option_id.clone(), o.name.clone()))
2583 .collect(),
2584 resolved: false,
2585 });
2586 self.needs_redraw = true;
2587 }
2588 AgentMessage::PromptComplete => {
2589 self.ai_inspector.chat.flush_agent_message();
2590 self.needs_redraw = true;
2591 }
2592 AgentMessage::ConfigUpdate { updates, reply } => {
2593 pending_config_updates.push((updates, reply));
2594 }
2595 AgentMessage::ClientReady(client) => {
2596 log::info!("ACP: agent_client ready");
2597 self.agent_client = Some(client);
2598 }
2599 }
2600 }
2601 }
2602 for (updates, reply) in pending_config_updates {
2604 let result = self.apply_agent_config_updates(&updates);
2605 if result.is_ok() {
2606 self.config_changed_by_agent = true;
2607 }
2608 let _ = reply.send(result);
2609 self.needs_redraw = true;
2610 }
2611
2612 if self.config.ai_inspector_agent_terminal_access {
2614 let new_messages = &self.ai_inspector.chat.messages[msg_count_before..];
2615 let commands_to_run: Vec<String> = new_messages
2616 .iter()
2617 .filter_map(|msg| {
2618 if let ChatMessage::CommandSuggestion(cmd) = msg {
2619 Some(format!("{cmd}\n"))
2620 } else {
2621 None
2622 }
2623 })
2624 .collect();
2625
2626 if !commands_to_run.is_empty()
2627 && let Some(tab) = self.tab_manager.active_tab()
2628 && let Ok(term) = tab.terminal.try_lock()
2629 {
2630 for cmd in &commands_to_run {
2631 let _ = term.write(cmd.as_bytes());
2632 }
2633 crate::debug_info!(
2634 "AI_INSPECTOR",
2635 "Auto-executed {} command(s) in terminal",
2636 commands_to_run.len()
2637 );
2638 }
2639 }
2640
2641 if self.ai_inspector.open
2645 && let Some(tab) = self.tab_manager.active_tab()
2646 && let Ok(term) = tab.terminal.try_lock()
2647 {
2648 let history = term.core_command_history();
2649 let current_count = history.len();
2650
2651 if current_count != self.ai_inspector.last_command_count {
2652 let had_commands = self.ai_inspector.last_command_count > 0;
2654 self.ai_inspector.last_command_count = current_count;
2655 self.ai_inspector.needs_refresh = true;
2656
2657 if had_commands
2659 && current_count > 0
2660 && self.config.ai_inspector_auto_context
2661 && self.ai_inspector.agent_status == AgentStatus::Connected
2662 && let Some((cmd, exit_code, duration_ms)) = history.last()
2663 {
2664 let exit_code_str = exit_code
2665 .map(|c| c.to_string())
2666 .unwrap_or_else(|| "N/A".to_string());
2667 let duration = duration_ms.unwrap_or(0);
2668
2669 let cwd = term.shell_integration_cwd().unwrap_or_default();
2670
2671 let context = format!(
2672 "Command completed:\n$ {}\nExit code: {}\nDuration: {}ms\nCWD: {}",
2673 cmd, exit_code_str, duration, cwd
2674 );
2675
2676 if let Some(agent) = &self.agent {
2677 let agent = agent.clone();
2678 let content = vec![par_term_acp::ContentBlock::Text { text: context }];
2679 self.runtime.spawn(async move {
2680 let agent = agent.lock().await;
2681 let _ = agent.send_prompt(content).await;
2682 });
2683 }
2684 }
2685 }
2686 }
2687
2688 if self.ai_inspector.open
2690 && self.ai_inspector.needs_refresh
2691 && let Some(tab) = self.tab_manager.active_tab()
2692 && let Ok(term) = tab.terminal.try_lock()
2693 {
2694 let snapshot = crate::ai_inspector::snapshot::SnapshotData::gather(
2695 &term,
2696 &self.ai_inspector.scope,
2697 self.config.ai_inspector_context_max_lines,
2698 );
2699 self.ai_inspector.snapshot = Some(snapshot);
2700 self.ai_inspector.needs_refresh = false;
2701 }
2702
2703 let is_tmux_gateway = self.is_gateway_active();
2708 let effective_pane_padding = if is_tmux_gateway {
2709 0.0
2710 } else {
2711 self.config.pane_padding
2712 };
2713
2714 let is_tmux_connected = self.is_tmux_connected();
2717 let status_bar_height =
2718 crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux_connected);
2719
2720 let custom_status_bar_height = self.status_bar_ui.height(&self.config, self.is_fullscreen);
2722
2723 let window_size_for_badge = self.renderer.as_ref().map(|r| r.size());
2725
2726 let progress_snapshot = if self.config.progress_bar_enabled {
2728 self.tab_manager.active_tab().and_then(|tab| {
2729 tab.terminal
2730 .try_lock()
2731 .ok()
2732 .map(|term| ProgressBarSnapshot {
2733 simple: term.progress_bar(),
2734 named: term.named_progress_bars(),
2735 })
2736 })
2737 } else {
2738 None
2739 };
2740
2741 self.sync_ai_inspector_width();
2744
2745 if let Some(renderer) = &mut self.renderer {
2746 let disable_cursor_shader =
2751 self.config.cursor_shader_disable_in_alt_screen && is_alt_screen;
2752 renderer.set_cursor_shader_disabled_for_alt_screen(disable_cursor_shader);
2753
2754 if !self.debug.cache_hit {
2757 let t = std::time::Instant::now();
2758 renderer.update_cells(&cells);
2759 debug_update_cells_time = t.elapsed();
2760 }
2761
2762 if let (Some(pos), Some(opacity), Some(style)) =
2764 (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
2765 {
2766 renderer.update_cursor(pos, opacity, style);
2767 let cursor_color = [
2770 self.config.cursor_color[0] as f32 / 255.0,
2771 self.config.cursor_color[1] as f32 / 255.0,
2772 self.config.cursor_color[2] as f32 / 255.0,
2773 1.0,
2774 ];
2775 renderer.update_shader_cursor(pos.0, pos.1, opacity, cursor_color, style);
2776 } else {
2777 renderer.clear_cursor();
2778 }
2779
2780 if let Some(ref snap) = progress_snapshot {
2782 use par_term_emu_core_rust::terminal::ProgressState;
2783 let state_val = match snap.simple.state {
2784 ProgressState::Hidden => 0.0,
2785 ProgressState::Normal => 1.0,
2786 ProgressState::Error => 2.0,
2787 ProgressState::Indeterminate => 3.0,
2788 ProgressState::Warning => 4.0,
2789 };
2790 let active_count = (if snap.simple.is_active() { 1 } else { 0 })
2791 + snap.named.values().filter(|b| b.state.is_active()).count();
2792 renderer.update_shader_progress(
2793 state_val,
2794 snap.simple.progress as f32 / 100.0,
2795 if snap.has_active() { 1.0 } else { 0.0 },
2796 active_count as f32,
2797 );
2798 } else {
2799 renderer.update_shader_progress(0.0, 0.0, 0.0, 0.0);
2800 }
2801
2802 let scroll_offset = self
2804 .tab_manager
2805 .active_tab()
2806 .map(|t| t.scroll_state.offset)
2807 .unwrap_or(0);
2808 renderer.update_scrollbar(scroll_offset, visible_lines, total_lines, &scrollback_marks);
2809
2810 if self.config.command_separator_enabled {
2812 let separator_marks = crate::renderer::compute_visible_separator_marks(
2813 &scrollback_marks,
2814 scrollback_len,
2815 scroll_offset,
2816 visible_lines,
2817 );
2818 renderer.set_separator_marks(separator_marks);
2819 } else {
2820 renderer.set_separator_marks(Vec::new());
2821 }
2822
2823 let anim_start = std::time::Instant::now();
2825 if let Some(tab) = self.tab_manager.active_tab() {
2826 let terminal = tab.terminal.blocking_lock();
2827 if terminal.update_animations() {
2828 if let Some(window) = &self.window {
2830 window.request_redraw();
2831 }
2832 }
2833 }
2834 let debug_anim_time = anim_start.elapsed();
2835
2836 let graphics_start = std::time::Instant::now();
2840 if let Some(tab) = self.tab_manager.active_tab() {
2841 let terminal = tab.terminal.blocking_lock();
2842 let mut graphics = terminal.get_graphics_with_animations();
2843 let scrollback_len = terminal.scrollback_len();
2844
2845 let scrollback_graphics = terminal.get_scrollback_graphics();
2847 let scrollback_count = scrollback_graphics.len();
2848 graphics.extend(scrollback_graphics);
2849
2850 debug_info!(
2851 "APP",
2852 "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
2853 graphics.len(),
2854 scrollback_count,
2855 scroll_offset,
2856 scrollback_len
2857 );
2858 if let Err(e) = renderer.update_graphics(
2859 &graphics,
2860 scroll_offset,
2861 scrollback_len,
2862 visible_lines,
2863 ) {
2864 log::error!("Failed to update graphics: {}", e);
2865 }
2866 }
2867 debug_graphics_time = graphics_start.elapsed();
2868
2869 let visual_bell_flash = self
2871 .tab_manager
2872 .active_tab()
2873 .and_then(|t| t.bell.visual_flash);
2874 let visual_bell_intensity = if let Some(flash_start) = visual_bell_flash {
2875 const FLASH_DURATION_MS: u128 = 150;
2876 let elapsed = flash_start.elapsed().as_millis();
2877 if elapsed < FLASH_DURATION_MS {
2878 if let Some(window) = &self.window {
2880 window.request_redraw();
2881 }
2882 0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
2884 } else {
2885 if let Some(tab) = self.tab_manager.active_tab_mut() {
2887 tab.bell.visual_flash = None;
2888 }
2889 0.0
2890 }
2891 } else {
2892 0.0
2893 };
2894
2895 renderer.set_visual_bell_intensity(visual_bell_intensity);
2897
2898 let egui_start = std::time::Instant::now();
2900
2901 let show_fps = self.debug.show_fps_overlay;
2903 let fps_value = self.debug.fps_value;
2904 let frame_time_ms = if !self.debug.frame_times.is_empty() {
2905 let avg = self.debug.frame_times.iter().sum::<std::time::Duration>()
2906 / self.debug.frame_times.len() as u32;
2907 avg.as_secs_f64() * 1000.0
2908 } else {
2909 0.0
2910 };
2911
2912 let badge_enabled = self.badge_state.enabled;
2914 let badge_state = if badge_enabled {
2915 if self.badge_state.is_dirty() {
2917 self.badge_state.interpolate();
2918 }
2919 Some(self.badge_state.clone())
2920 } else {
2921 None
2922 };
2923
2924 let status_bar_session_vars = if self.config.status_bar_enabled {
2926 Some(self.badge_state.variables.read().clone())
2927 } else {
2928 None
2929 };
2930
2931 let hovered_mark: Option<crate::scrollback_metadata::ScrollbackMark> =
2933 if self.config.scrollbar_mark_tooltips && self.config.scrollbar_command_marks {
2934 self.tab_manager
2935 .active_tab()
2936 .map(|tab| tab.mouse.position)
2937 .and_then(|(mx, my)| {
2938 renderer.scrollbar_mark_at_position(mx as f32, my as f32, 8.0)
2939 })
2940 .cloned()
2941 } else {
2942 None
2943 };
2944
2945 let pane_identify_bounds: Vec<(usize, crate::pane::PaneBounds)> =
2947 if self.pane_identify_hide_time.is_some() {
2948 self.tab_manager
2949 .active_tab()
2950 .and_then(|tab| tab.pane_manager())
2951 .map(|pm| {
2952 pm.all_panes()
2953 .iter()
2954 .enumerate()
2955 .map(|(i, pane)| (i, pane.bounds))
2956 .collect()
2957 })
2958 .unwrap_or_default()
2959 } else {
2960 Vec::new()
2961 };
2962
2963 let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
2964 (&self.egui_ctx, &mut self.egui_state)
2965 {
2966 let mut raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
2967
2968 raw_input.events.append(&mut self.pending_egui_events);
2971
2972 let any_modal_visible = self.help_ui.visible
2978 || self.clipboard_history_ui.visible
2979 || self.command_history_ui.visible
2980 || self.shader_install_ui.visible
2981 || self.integrations_ui.visible
2982 || self.search_ui.visible
2983 || self.tmux_session_picker_ui.visible
2984 || self.ssh_connect_ui.is_visible()
2985 || self.quit_confirmation_ui.is_visible()
2986 || self.remote_shell_install_ui.is_visible();
2987 if !any_modal_visible {
2988 raw_input.events.retain(|e| {
2989 !matches!(
2990 e,
2991 egui::Event::Key {
2992 key: egui::Key::Tab,
2993 ..
2994 }
2995 )
2996 });
2997 }
2998
2999 let egui_output = egui_ctx.run(raw_input, |ctx| {
3000 if show_fps {
3002 egui::Area::new(egui::Id::new("fps_overlay"))
3003 .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
3004 .order(egui::Order::Foreground)
3005 .show(ctx, |ui| {
3006 egui::Frame::NONE
3007 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
3008 .inner_margin(egui::Margin::same(8))
3009 .corner_radius(4.0)
3010 .show(ui, |ui| {
3011 ui.style_mut().visuals.override_text_color =
3012 Some(egui::Color32::from_rgb(0, 255, 0));
3013 ui.label(
3014 egui::RichText::new(format!(
3015 "FPS: {:.1}\nFrame: {:.2}ms",
3016 fps_value, frame_time_ms
3017 ))
3018 .monospace()
3019 .size(14.0),
3020 );
3021 });
3022 });
3023 }
3024
3025 if self.resize_overlay_visible
3027 && let Some((width_px, height_px, cols, rows)) = self.resize_dimensions
3028 {
3029 egui::Area::new(egui::Id::new("resize_overlay"))
3030 .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
3031 .order(egui::Order::Foreground)
3032 .show(ctx, |ui| {
3033 egui::Frame::NONE
3034 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 220))
3035 .inner_margin(egui::Margin::same(16))
3036 .corner_radius(8.0)
3037 .show(ui, |ui| {
3038 ui.style_mut().visuals.override_text_color =
3039 Some(egui::Color32::from_rgb(255, 255, 255));
3040 ui.label(
3041 egui::RichText::new(format!(
3042 "{}×{}\n{}×{} px",
3043 cols, rows, width_px, height_px
3044 ))
3045 .monospace()
3046 .size(24.0),
3047 );
3048 });
3049 });
3050 }
3051
3052 if self.copy_mode.active && self.config.copy_mode_show_status {
3054 let status = self.copy_mode.status_text();
3055 let (mode_text, color) = if self.copy_mode.is_searching {
3056 ("SEARCH", egui::Color32::from_rgb(255, 165, 0))
3057 } else {
3058 match self.copy_mode.visual_mode {
3059 crate::copy_mode::VisualMode::None => {
3060 ("COPY", egui::Color32::from_rgb(100, 200, 100))
3061 }
3062 crate::copy_mode::VisualMode::Char => {
3063 ("VISUAL", egui::Color32::from_rgb(100, 150, 255))
3064 }
3065 crate::copy_mode::VisualMode::Line => {
3066 ("V-LINE", egui::Color32::from_rgb(100, 150, 255))
3067 }
3068 crate::copy_mode::VisualMode::Block => {
3069 ("V-BLOCK", egui::Color32::from_rgb(100, 150, 255))
3070 }
3071 }
3072 };
3073
3074 egui::Area::new(egui::Id::new("copy_mode_status_bar"))
3075 .anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(0.0, 0.0))
3076 .order(egui::Order::Foreground)
3077 .show(ctx, |ui| {
3078 let available_width = ui.available_width();
3079 egui::Frame::NONE
3080 .fill(egui::Color32::from_rgba_unmultiplied(40, 40, 40, 230))
3081 .inner_margin(egui::Margin::symmetric(12, 6))
3082 .show(ui, |ui| {
3083 ui.set_min_width(available_width);
3084 ui.horizontal(|ui| {
3085 ui.label(
3086 egui::RichText::new(mode_text)
3087 .monospace()
3088 .size(13.0)
3089 .color(color)
3090 .strong(),
3091 );
3092 ui.separator();
3093 ui.label(
3094 egui::RichText::new(&status)
3095 .monospace()
3096 .size(12.0)
3097 .color(egui::Color32::from_rgb(200, 200, 200)),
3098 );
3099 });
3100 });
3101 });
3102 }
3103
3104 if let Some(ref message) = self.toast_message {
3106 egui::Area::new(egui::Id::new("toast_notification"))
3107 .anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, 60.0))
3108 .order(egui::Order::Foreground)
3109 .show(ctx, |ui| {
3110 egui::Frame::NONE
3111 .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
3112 .inner_margin(egui::Margin::symmetric(20, 12))
3113 .corner_radius(8.0)
3114 .stroke(egui::Stroke::new(
3115 1.0,
3116 egui::Color32::from_rgb(80, 80, 80),
3117 ))
3118 .show(ui, |ui| {
3119 ui.style_mut().visuals.override_text_color =
3120 Some(egui::Color32::from_rgb(255, 255, 255));
3121 ui.label(egui::RichText::new(message).size(16.0));
3122 });
3123 });
3124 }
3125
3126 if let Some(ref mark) = hovered_mark {
3128 let mut lines = Vec::new();
3130
3131 if let Some(ref cmd) = mark.command {
3132 let truncated = if cmd.len() > 50 {
3133 format!("{}...", &cmd[..47])
3134 } else {
3135 cmd.clone()
3136 };
3137 lines.push(format!("Command: {}", truncated));
3138 }
3139
3140 if let Some(start_time) = mark.start_time {
3141 use chrono::{DateTime, Local, Utc};
3142 let dt =
3143 DateTime::<Utc>::from_timestamp_millis(start_time as i64).unwrap();
3144 let local: DateTime<Local> = dt.into();
3145 lines.push(format!("Time: {}", local.format("%H:%M:%S")));
3146 }
3147
3148 if let Some(duration_ms) = mark.duration_ms {
3149 if duration_ms < 1000 {
3150 lines.push(format!("Duration: {}ms", duration_ms));
3151 } else if duration_ms < 60000 {
3152 lines
3153 .push(format!("Duration: {:.1}s", duration_ms as f64 / 1000.0));
3154 } else {
3155 let mins = duration_ms / 60000;
3156 let secs = (duration_ms % 60000) / 1000;
3157 lines.push(format!("Duration: {}m {}s", mins, secs));
3158 }
3159 }
3160
3161 if let Some(exit_code) = mark.exit_code {
3162 lines.push(format!("Exit: {}", exit_code));
3163 }
3164
3165 let tooltip_text = lines.join("\n");
3166
3167 let mouse_pos = ctx.pointer_hover_pos().unwrap_or(egui::pos2(100.0, 100.0));
3169 let tooltip_x = (mouse_pos.x - 180.0).max(10.0);
3170 let tooltip_y = (mouse_pos.y - 20.0).max(10.0);
3171
3172 egui::Area::new(egui::Id::new("scrollbar_mark_tooltip"))
3174 .order(egui::Order::Tooltip)
3175 .fixed_pos(egui::pos2(tooltip_x, tooltip_y))
3176 .show(ctx, |ui| {
3177 ui.set_min_width(150.0);
3178 egui::Frame::NONE
3179 .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
3180 .inner_margin(egui::Margin::same(8))
3181 .corner_radius(4.0)
3182 .stroke(egui::Stroke::new(
3183 1.0,
3184 egui::Color32::from_rgb(80, 80, 80),
3185 ))
3186 .show(ui, |ui| {
3187 ui.set_min_width(140.0);
3188 ui.style_mut().visuals.override_text_color =
3189 Some(egui::Color32::from_rgb(220, 220, 220));
3190 ui.label(
3191 egui::RichText::new(&tooltip_text)
3192 .monospace()
3193 .size(12.0),
3194 );
3195 });
3196 });
3197 }
3198
3199 pending_tab_action = self.tab_bar_ui.render(
3201 ctx,
3202 &self.tab_manager,
3203 &self.config,
3204 &self.profile_manager,
3205 );
3206
3207 self.tmux_status_bar_ui.render(
3209 ctx,
3210 &self.config,
3211 self.tmux_session.as_ref(),
3212 self.tmux_session_name.as_deref(),
3213 );
3214
3215 if let Some(ref session_vars) = status_bar_session_vars {
3217 let (_bar_height, status_bar_action) = self.status_bar_ui.render(
3218 ctx,
3219 &self.config,
3220 session_vars,
3221 self.is_fullscreen,
3222 );
3223 if status_bar_action
3224 == Some(crate::status_bar::StatusBarAction::ShowUpdateDialog)
3225 {
3226 self.show_update_dialog = true;
3227 }
3228 }
3229
3230 self.help_ui.show(ctx);
3235
3236 pending_clipboard_action = self.clipboard_history_ui.show(ctx);
3238
3239 pending_command_history_action = self.command_history_ui.show(ctx);
3241
3242 pending_paste_special_action = self.paste_special_ui.show(ctx);
3244
3245 pending_search_action = self.search_ui.show(ctx, visible_lines, scrollback_len);
3247
3248 pending_inspector_action = self.ai_inspector.show(ctx, &self.available_agents);
3250
3251 let tmux_path = self.config.resolve_tmux_path();
3253 pending_session_picker_action =
3254 self.tmux_session_picker_ui.show(ctx, &tmux_path);
3255
3256 pending_shader_install_response = self.shader_install_ui.show(ctx);
3258
3259 pending_integrations_response = self.integrations_ui.show(ctx);
3261
3262 pending_close_confirm_action = self.close_confirmation_ui.show(ctx);
3264
3265 pending_quit_confirm_action = self.quit_confirmation_ui.show(ctx);
3267
3268 pending_remote_install_action = self.remote_shell_install_ui.show(ctx);
3270
3271 pending_ssh_connect_action = self.ssh_connect_ui.show(ctx);
3273
3274 if self.show_update_dialog {
3276 if let Some(ref rx) = self.update_install_receiver
3278 && let Ok(result) = rx.try_recv()
3279 {
3280 match result {
3281 Ok(update_result) => {
3282 self.update_install_status = Some(format!(
3283 "Updated to v{}! Restart par-term to use the new version.",
3284 update_result.new_version
3285 ));
3286 self.update_installing = false;
3287 self.status_bar_ui.update_available_version = None;
3288 }
3289 Err(e) => {
3290 self.update_install_status =
3291 Some(format!("Update failed: {}", e));
3292 self.update_installing = false;
3293 }
3294 }
3295 self.update_install_receiver = None;
3296 }
3297
3298 if let Some(ref update_result) = self.last_update_result {
3299 let dialog_action = crate::update_dialog::render(
3300 ctx,
3301 update_result,
3302 env!("CARGO_PKG_VERSION"),
3303 self.installation_type,
3304 self.update_installing,
3305 self.update_install_status.as_deref(),
3306 );
3307 match dialog_action {
3308 crate::update_dialog::UpdateDialogAction::Dismiss => {
3309 if !self.update_installing {
3310 self.show_update_dialog = false;
3311 self.update_install_status = None;
3312 }
3313 }
3314 crate::update_dialog::UpdateDialogAction::SkipVersion(v) => {
3315 self.config.skipped_version = Some(v);
3316 self.show_update_dialog = false;
3317 self.status_bar_ui.update_available_version = None;
3318 self.update_install_status = None;
3319 let _ = self.config.save();
3320 }
3321 crate::update_dialog::UpdateDialogAction::InstallUpdate(v) => {
3322 if !self.update_installing {
3323 self.update_installing = true;
3324 self.update_install_status =
3325 Some("Downloading update...".to_string());
3326 let (tx, rx) = std::sync::mpsc::channel();
3327 self.update_install_receiver = Some(rx);
3328 let version = v.clone();
3329 std::thread::spawn(move || {
3330 let result =
3331 crate::self_updater::perform_update(&version);
3332 let _ = tx.send(result);
3333 });
3334 }
3335 }
3337 crate::update_dialog::UpdateDialogAction::None => {}
3338 }
3339 } else {
3340 self.show_update_dialog = false;
3342 }
3343 }
3344
3345 pending_profile_drawer_action = self.profile_drawer_ui.render(
3347 ctx,
3348 &self.profile_manager,
3349 &self.config,
3350 false, );
3352
3353 if let (Some(snap), Some(size)) = (&progress_snapshot, window_size_for_badge) {
3355 render_progress_bars(
3356 ctx,
3357 snap,
3358 &self.config,
3359 size.width as f32,
3360 size.height as f32,
3361 );
3362 }
3363
3364 if !pane_identify_bounds.is_empty() {
3366 for (index, bounds) in &pane_identify_bounds {
3367 let center_x = bounds.x + bounds.width / 2.0;
3368 let center_y = bounds.y + bounds.height / 2.0;
3369 egui::Area::new(egui::Id::new(format!("pane_identify_{}", index)))
3370 .fixed_pos(egui::pos2(center_x - 30.0, center_y - 30.0))
3371 .order(egui::Order::Foreground)
3372 .interactable(false)
3373 .show(ctx, |ui| {
3374 egui::Frame::NONE
3375 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
3376 .inner_margin(egui::Margin::symmetric(16, 8))
3377 .corner_radius(8.0)
3378 .stroke(egui::Stroke::new(
3379 2.0,
3380 egui::Color32::from_rgb(100, 200, 255),
3381 ))
3382 .show(ui, |ui| {
3383 ui.label(
3384 egui::RichText::new(format!("Pane {}", index))
3385 .monospace()
3386 .size(28.0)
3387 .color(egui::Color32::from_rgb(100, 200, 255)),
3388 );
3389 });
3390 });
3391 }
3392 }
3393
3394 crate::app::file_transfers::render_file_transfer_overlay(
3396 &self.file_transfer_state,
3397 ctx,
3398 );
3399
3400 if let (Some(badge), Some(size)) = (&badge_state, window_size_for_badge) {
3402 render_badge(ctx, badge, size.width as f32, size.height as f32);
3403 }
3404 });
3405
3406 egui_state.handle_platform_output(
3409 self.window.as_ref().unwrap(),
3410 egui_output.platform_output.clone(),
3411 );
3412
3413 Some((egui_output, egui_ctx))
3414 } else {
3415 None
3416 };
3417
3418 if !self.egui_initialized && egui_data.is_some() {
3420 self.egui_initialized = true;
3421 }
3422
3423 let debug_egui_time = egui_start.elapsed();
3427
3428 let avg_frame_time = if !self.debug.frame_times.is_empty() {
3430 self.debug.frame_times.iter().sum::<std::time::Duration>()
3431 / self.debug.frame_times.len() as u32
3432 } else {
3433 std::time::Duration::ZERO
3434 };
3435 let fps = if avg_frame_time.as_secs_f64() > 0.0 {
3436 1.0 / avg_frame_time.as_secs_f64()
3437 } else {
3438 0.0
3439 };
3440
3441 self.debug.fps_value = fps;
3443
3444 if self.debug.frame_times.len() >= 60 {
3446 let (cache_gen, cache_has_cells) = self
3447 .tab_manager
3448 .active_tab()
3449 .map(|t| (t.cache.generation, t.cache.cells.is_some()))
3450 .unwrap_or((0, false));
3451 log::info!(
3452 "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={}",
3453 fps,
3454 avg_frame_time.as_secs_f64() * 1000.0,
3455 self.debug.cell_gen_time.as_secs_f64() * 1000.0,
3456 if self.debug.cache_hit { "HIT" } else { "MISS" },
3457 debug_url_detect_time.as_secs_f64() * 1000.0,
3458 debug_anim_time.as_secs_f64() * 1000.0,
3459 debug_graphics_time.as_secs_f64() * 1000.0,
3460 debug_egui_time.as_secs_f64() * 1000.0,
3461 debug_update_cells_time.as_secs_f64() * 1000.0,
3462 debug_actual_render_time.as_secs_f64() * 1000.0,
3463 self.debug.render_time.as_secs_f64() * 1000.0,
3464 cells.len(),
3465 cache_gen,
3466 if cache_has_cells { "YES" } else { "NO" }
3467 );
3468 }
3469
3470 let actual_render_start = std::time::Instant::now();
3472 let sizing = RendererSizing {
3476 size: renderer.size(),
3477 content_offset_y: renderer.content_offset_y(),
3478 content_offset_x: renderer.content_offset_x(),
3479 content_inset_bottom: renderer.content_inset_bottom(),
3480 content_inset_right: renderer.content_inset_right(),
3481 cell_width: renderer.cell_width(),
3482 cell_height: renderer.cell_height(),
3483 padding: renderer.window_padding(),
3484 status_bar_height: (status_bar_height + custom_status_bar_height)
3485 * renderer.scale_factor(),
3486 scale_factor: renderer.scale_factor(),
3487 };
3488
3489 let (has_pane_manager, pane_count) = self
3495 .tab_manager
3496 .active_tab()
3497 .and_then(|t| t.pane_manager.as_ref())
3498 .map(|pm| (pm.pane_count() > 0, pm.pane_count()))
3499 .unwrap_or((false, 0));
3500
3501 crate::debug_trace!(
3502 "RENDER",
3503 "has_pane_manager={}, pane_count={}",
3504 has_pane_manager,
3505 pane_count
3506 );
3507
3508 let pane_0_bg: Option<crate::pane::PaneBackground> = None;
3511
3512 let render_result = if has_pane_manager {
3513 let effective_padding =
3516 if pane_count > 1 && self.config.hide_window_padding_on_split {
3517 0.0
3518 } else {
3519 sizing.padding
3520 };
3521
3522 let content_width = sizing.size.width as f32
3524 - effective_padding * 2.0
3525 - sizing.content_offset_x
3526 - sizing.content_inset_right;
3527 let content_height = sizing.size.height as f32
3528 - sizing.content_offset_y
3529 - sizing.content_inset_bottom
3530 - effective_padding
3531 - sizing.status_bar_height;
3532
3533 #[allow(clippy::type_complexity)]
3535 let pane_render_data: Option<(
3536 Vec<PaneRenderData>,
3537 Vec<crate::pane::DividerRect>,
3538 Vec<PaneTitleInfo>,
3539 Option<PaneViewport>,
3540 )> = {
3541 let tab = self.tab_manager.active_tab_mut();
3542 if let Some(tab) = tab {
3543 if let Some(pm) = &mut tab.pane_manager {
3544 let bounds = crate::pane::PaneBounds::new(
3546 effective_padding + sizing.content_offset_x,
3547 sizing.content_offset_y,
3548 content_width,
3549 content_height,
3550 );
3551 pm.set_bounds(bounds);
3552
3553 let title_height_offset = if self.config.show_pane_titles {
3556 self.config.pane_title_height * sizing.scale_factor
3557 } else {
3558 0.0
3559 };
3560
3561 pm.resize_all_terminals_with_padding(
3564 sizing.cell_width,
3565 sizing.cell_height,
3566 effective_pane_padding * sizing.scale_factor,
3567 title_height_offset,
3568 );
3569
3570 let focused_pane_id = pm.focused_pane_id();
3572 let all_pane_ids: Vec<_> =
3573 pm.all_panes().iter().map(|p| p.id).collect();
3574 let dividers = pm.get_dividers();
3575
3576 let pane_bg_opacity = self.config.pane_background_opacity;
3577 let inactive_opacity = if self.config.dim_inactive_panes {
3578 self.config.inactive_pane_opacity
3579 } else {
3580 1.0
3581 };
3582 let cursor_opacity = self.cursor_opacity;
3583
3584 let show_titles = self.config.show_pane_titles;
3587 let title_height = self.config.pane_title_height * sizing.scale_factor;
3588 let title_position = self.config.pane_title_position;
3589 let title_text_color = [
3590 self.config.pane_title_color[0] as f32 / 255.0,
3591 self.config.pane_title_color[1] as f32 / 255.0,
3592 self.config.pane_title_color[2] as f32 / 255.0,
3593 ];
3594 let title_bg_color = [
3595 self.config.pane_title_bg_color[0] as f32 / 255.0,
3596 self.config.pane_title_bg_color[1] as f32 / 255.0,
3597 self.config.pane_title_bg_color[2] as f32 / 255.0,
3598 ];
3599
3600 let mut pane_data = Vec::new();
3601 let mut pane_titles = Vec::new();
3602 let mut focused_viewport: Option<PaneViewport> = None;
3603
3604 for pane_id in &all_pane_ids {
3605 if let Some(pane) = pm.get_pane(*pane_id) {
3606 let is_focused = Some(*pane_id) == focused_pane_id;
3607 let bounds = pane.bounds;
3608
3609 let (viewport_y, viewport_height) = if show_titles {
3611 use crate::config::PaneTitlePosition;
3612 match title_position {
3613 PaneTitlePosition::Top => (
3614 bounds.y + title_height,
3615 (bounds.height - title_height).max(0.0),
3616 ),
3617 PaneTitlePosition::Bottom => {
3618 (bounds.y, (bounds.height - title_height).max(0.0))
3619 }
3620 }
3621 } else {
3622 (bounds.y, bounds.height)
3623 };
3624
3625 let physical_pane_padding =
3628 effective_pane_padding * sizing.scale_factor;
3629 let viewport = PaneViewport::with_padding(
3630 bounds.x,
3631 viewport_y,
3632 bounds.width,
3633 viewport_height,
3634 is_focused,
3635 if is_focused {
3636 pane_bg_opacity
3637 } else {
3638 pane_bg_opacity * inactive_opacity
3639 },
3640 physical_pane_padding,
3641 );
3642
3643 if is_focused {
3644 focused_viewport = Some(viewport);
3645 }
3646
3647 if show_titles {
3649 use crate::config::PaneTitlePosition;
3650 let title_y = match title_position {
3651 PaneTitlePosition::Top => bounds.y,
3652 PaneTitlePosition::Bottom => {
3653 bounds.y + bounds.height - title_height
3654 }
3655 };
3656 pane_titles.push(PaneTitleInfo {
3657 x: bounds.x,
3658 y: title_y,
3659 width: bounds.width,
3660 height: title_height,
3661 title: pane.get_title(),
3662 focused: is_focused,
3663 text_color: title_text_color,
3664 bg_color: title_bg_color,
3665 });
3666 }
3667
3668 let cells = if let Ok(term) = pane.terminal.try_lock() {
3669 let scroll_offset = pane.scroll_state.offset;
3670 let selection =
3671 pane.mouse.selection.map(|sel| sel.normalized());
3672 let rectangular = pane
3673 .mouse
3674 .selection
3675 .map(|sel| sel.mode == SelectionMode::Rectangular)
3676 .unwrap_or(false);
3677 term.get_cells_with_scrollback(
3678 scroll_offset,
3679 selection,
3680 rectangular,
3681 None,
3682 )
3683 } else {
3684 Vec::new()
3685 };
3686
3687 let need_marks = self.config.scrollbar_command_marks
3688 || self.config.command_separator_enabled;
3689 let (marks, pane_scrollback_len) = if need_marks {
3690 if let Ok(mut term) = pane.terminal.try_lock() {
3691 let sb_len = term.scrollback_len();
3693 term.update_scrollback_metadata(sb_len, 0);
3694 (term.scrollback_marks(), sb_len)
3695 } else {
3696 (Vec::new(), 0)
3697 }
3698 } else {
3699 (Vec::new(), 0)
3700 };
3701 let pane_scroll_offset = pane.scroll_state.offset;
3702
3703 let pane_background = if all_pane_ids.len() > 1
3705 && pane.background().has_image()
3706 {
3707 Some(pane.background().clone())
3708 } else {
3709 None
3710 };
3711
3712 let cursor_pos = if let Ok(term) = pane.terminal.try_lock() {
3713 if term.is_cursor_visible() {
3714 Some(term.cursor_position())
3715 } else {
3716 None
3717 }
3718 } else {
3719 None
3720 };
3721
3722 let content_width = (bounds.width
3725 - physical_pane_padding * 2.0)
3726 .max(sizing.cell_width);
3727 let content_height = (viewport_height
3728 - physical_pane_padding * 2.0)
3729 .max(sizing.cell_height);
3730 let cols = (content_width / sizing.cell_width).floor() as usize;
3731 let rows =
3732 (content_height / sizing.cell_height).floor() as usize;
3733 let cols = cols.max(1);
3734 let rows = rows.max(1);
3735
3736 pane_data.push((
3737 viewport,
3738 cells,
3739 (cols, rows),
3740 cursor_pos,
3741 if is_focused { cursor_opacity } else { 0.0 },
3742 marks,
3743 pane_scrollback_len,
3744 pane_scroll_offset,
3745 pane_background,
3746 ));
3747 }
3748 }
3749
3750 Some((pane_data, dividers, pane_titles, focused_viewport))
3751 } else {
3752 None
3753 }
3754 } else {
3755 None
3756 }
3757 };
3758
3759 if let Some((pane_data, dividers, pane_titles, focused_viewport)) = pane_render_data
3760 {
3761 let hovered_divider_index = self
3763 .tab_manager
3764 .active_tab()
3765 .and_then(|t| t.mouse.hovered_divider_index);
3766
3767 Self::render_split_panes_with_data(
3769 renderer,
3770 pane_data,
3771 dividers,
3772 pane_titles,
3773 focused_viewport,
3774 &self.config,
3775 egui_data,
3776 hovered_divider_index,
3777 )
3778 } else {
3779 renderer.render(egui_data, false, show_scrollbar, pane_0_bg.as_ref())
3781 }
3782 } else {
3783 renderer.render(egui_data, false, show_scrollbar, pane_0_bg.as_ref())
3785 };
3786
3787 match render_result {
3788 Ok(rendered) => {
3789 if !rendered {
3790 log::trace!("Skipped rendering - no changes");
3791 }
3792 }
3793 Err(e) => {
3794 if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
3797 match surface_error {
3798 SurfaceError::Outdated | SurfaceError::Lost => {
3799 log::warn!(
3800 "Surface error detected ({:?}), reconfiguring...",
3801 surface_error
3802 );
3803 self.force_surface_reconfigure();
3804 }
3805 SurfaceError::Timeout => {
3806 log::warn!("Surface timeout, will retry next frame");
3807 if let Some(window) = &self.window {
3808 window.request_redraw();
3809 }
3810 }
3811 SurfaceError::OutOfMemory => {
3812 log::error!("Surface out of memory: {:?}", surface_error);
3813 }
3814 _ => {
3815 log::error!("Surface error: {:?}", surface_error);
3816 }
3817 }
3818 } else {
3819 log::error!("Render error: {}", e);
3820 }
3821 }
3822 }
3823 debug_actual_render_time = actual_render_start.elapsed();
3824 let _ = debug_actual_render_time;
3825
3826 self.debug.render_time = render_start.elapsed();
3827 }
3828
3829 self.sync_ai_inspector_width();
3833
3834 match pending_tab_action {
3837 TabBarAction::SwitchTo(id) => {
3838 self.tab_manager.switch_to(id);
3839 if let Some(renderer) = &mut self.renderer {
3841 renderer.clear_all_cells();
3842 }
3843 if let Some(tab) = self.tab_manager.active_tab_mut() {
3844 tab.cache.cells = None;
3845 }
3846 self.needs_redraw = true;
3847 if let Some(window) = &self.window {
3848 window.request_redraw();
3849 }
3850 }
3851 TabBarAction::Close(id) => {
3852 self.tab_manager.switch_to(id);
3856 let was_last = self.close_current_tab();
3857 if was_last {
3858 self.is_shutting_down = true;
3859 }
3860 if let Some(window) = &self.window {
3861 window.request_redraw();
3862 }
3863 }
3864 TabBarAction::NewTab => {
3865 self.new_tab();
3866 if let Some(window) = &self.window {
3867 window.request_redraw();
3868 }
3869 }
3870 TabBarAction::SetColor(id, color) => {
3871 if let Some(tab) = self.tab_manager.get_tab_mut(id) {
3872 tab.set_custom_color(color);
3873 log::info!(
3874 "Set custom color for tab {}: RGB({}, {}, {})",
3875 id,
3876 color[0],
3877 color[1],
3878 color[2]
3879 );
3880 }
3881 if let Some(window) = &self.window {
3882 window.request_redraw();
3883 }
3884 }
3885 TabBarAction::ClearColor(id) => {
3886 if let Some(tab) = self.tab_manager.get_tab_mut(id) {
3887 tab.clear_custom_color();
3888 log::info!("Cleared custom color for tab {}", id);
3889 }
3890 if let Some(window) = &self.window {
3891 window.request_redraw();
3892 }
3893 }
3894 TabBarAction::Reorder(id, target_index) => {
3895 if self.tab_manager.move_tab_to_index(id, target_index) {
3896 self.needs_redraw = true;
3897 if let Some(window) = &self.window {
3898 window.request_redraw();
3899 }
3900 }
3901 }
3902 TabBarAction::NewTabWithProfile(profile_id) => {
3903 self.open_profile(profile_id);
3904 if let Some(window) = &self.window {
3905 window.request_redraw();
3906 }
3907 }
3908 TabBarAction::Duplicate(id) => {
3909 self.duplicate_tab_by_id(id);
3910 if let Some(window) = &self.window {
3911 window.request_redraw();
3912 }
3913 }
3914 TabBarAction::ToggleAssistantPanel => {
3915 let just_opened = self.ai_inspector.toggle();
3916 self.sync_ai_inspector_width();
3917 if just_opened {
3918 self.try_auto_connect_agent();
3919 }
3920 if let Some(window) = &self.window {
3921 window.request_redraw();
3922 }
3923 }
3924 TabBarAction::None => {}
3925 }
3926
3927 match pending_clipboard_action {
3930 ClipboardHistoryAction::Paste(content) => {
3931 self.paste_text(&content);
3932 }
3933 ClipboardHistoryAction::ClearAll => {
3934 if let Some(tab) = self.tab_manager.active_tab()
3935 && let Ok(term) = tab.terminal.try_lock()
3936 {
3937 term.clear_all_clipboard_history();
3938 log::info!("Cleared all clipboard history");
3939 }
3940 self.clipboard_history_ui.update_entries(Vec::new());
3941 }
3942 ClipboardHistoryAction::ClearSlot(slot) => {
3943 if let Some(tab) = self.tab_manager.active_tab()
3944 && let Ok(term) = tab.terminal.try_lock()
3945 {
3946 term.clear_clipboard_history(slot);
3947 log::info!("Cleared clipboard history for slot {:?}", slot);
3948 }
3949 }
3950 ClipboardHistoryAction::None => {}
3951 }
3952
3953 match pending_command_history_action {
3955 CommandHistoryAction::Insert(command) => {
3956 self.paste_text(&command);
3957 log::info!(
3958 "Inserted command from history: {}",
3959 &command[..command.len().min(60)]
3960 );
3961 }
3962 CommandHistoryAction::None => {}
3963 }
3964
3965 match pending_close_confirm_action {
3967 CloseConfirmAction::Close { tab_id, pane_id } => {
3968 if let Some(pane_id) = pane_id {
3970 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
3972 && let Some(pm) = tab.pane_manager_mut()
3973 {
3974 pm.close_pane(pane_id);
3975 log::info!("Force-closed pane {} in tab {}", pane_id, tab_id);
3976 }
3977 } else {
3978 self.tab_manager.close_tab(tab_id);
3980 log::info!("Force-closed tab {}", tab_id);
3981 }
3982 self.needs_redraw = true;
3983 if let Some(window) = &self.window {
3984 window.request_redraw();
3985 }
3986 }
3987 CloseConfirmAction::Cancel => {
3988 log::debug!("Close confirmation cancelled");
3990 }
3991 CloseConfirmAction::None => {}
3992 }
3993
3994 match pending_quit_confirm_action {
3996 QuitConfirmAction::Quit => {
3997 log::info!("Quit confirmed by user");
3999 self.perform_shutdown();
4000 }
4001 QuitConfirmAction::Cancel => {
4002 log::debug!("Quit confirmation cancelled");
4003 }
4004 QuitConfirmAction::None => {}
4005 }
4006
4007 match pending_remote_install_action {
4009 RemoteShellInstallAction::Install => {
4010 let command = RemoteShellInstallUI::install_command();
4014 self.paste_text(&format!("{}\n", command));
4016 if let Some(window) = &self.window {
4017 window.request_redraw();
4018 }
4019 }
4020 RemoteShellInstallAction::Cancel => {
4021 if let Some(window) = &self.window {
4022 window.request_redraw();
4023 }
4024 }
4025 RemoteShellInstallAction::None => {}
4026 }
4027
4028 match pending_ssh_connect_action {
4030 SshConnectAction::Connect {
4031 host,
4032 profile_override: _,
4033 } => {
4034 let args = host.ssh_args();
4036 let ssh_cmd = format!("ssh {}\n", args.join(" "));
4037 if let Some(tab) = self.tab_manager.active_tab()
4038 && let Ok(term) = tab.terminal.try_lock()
4039 {
4040 let _ = term.write_str(&ssh_cmd);
4041 }
4042 log::info!(
4043 "SSH Quick Connect: connecting to {}",
4044 host.connection_string()
4045 );
4046 if let Some(window) = &self.window {
4047 window.request_redraw();
4048 }
4049 }
4050 SshConnectAction::Cancel => {
4051 if let Some(window) = &self.window {
4052 window.request_redraw();
4053 }
4054 }
4055 SshConnectAction::None => {}
4056 }
4057
4058 match pending_paste_special_action {
4060 PasteSpecialAction::Paste(content) => {
4061 self.paste_text(&content);
4062 log::debug!("Pasted transformed text ({} chars)", content.len());
4063 }
4064 PasteSpecialAction::None => {}
4065 }
4066
4067 match pending_search_action {
4069 crate::search::SearchAction::ScrollToMatch(offset) => {
4070 self.set_scroll_target(offset);
4071 self.needs_redraw = true;
4072 if let Some(window) = &self.window {
4073 window.request_redraw();
4074 }
4075 }
4076 crate::search::SearchAction::Close => {
4077 self.needs_redraw = true;
4078 if let Some(window) = &self.window {
4079 window.request_redraw();
4080 }
4081 }
4082 crate::search::SearchAction::None => {}
4083 }
4084
4085 match pending_inspector_action {
4087 InspectorAction::Close => {
4088 self.ai_inspector.open = false;
4089 self.sync_ai_inspector_width();
4090 }
4091 InspectorAction::CopyJson(json) => {
4092 if let Ok(mut clipboard) = arboard::Clipboard::new() {
4093 let _ = clipboard.set_text(json);
4094 }
4095 }
4096 InspectorAction::SaveToFile(json) => {
4097 if let Some(path) = rfd::FileDialog::new()
4098 .set_file_name(format!(
4099 "par-term-snapshot-{}.json",
4100 chrono::Local::now().format("%Y-%m-%d-%H%M%S")
4101 ))
4102 .add_filter("JSON", &["json"])
4103 .save_file()
4104 {
4105 let _ = std::fs::write(path, json);
4106 }
4107 }
4108 InspectorAction::WriteToTerminal(cmd) => {
4109 if let Some(tab) = self.tab_manager.active_tab()
4110 && let Ok(term) = tab.terminal.try_lock()
4111 {
4112 let _ = term.write(cmd.as_bytes());
4113 }
4114 }
4115 InspectorAction::RunCommandAndNotify(cmd) => {
4116 if let Some(tab) = self.tab_manager.active_tab()
4118 && let Ok(term) = tab.terminal.try_lock()
4119 {
4120 let _ = term.write(format!("{cmd}\n").as_bytes());
4121 }
4122 let history_len = self
4124 .tab_manager
4125 .active_tab()
4126 .and_then(|tab| tab.terminal.try_lock().ok())
4127 .map(|term| term.core_command_history().len())
4128 .unwrap_or(0);
4129 if let Some(agent) = &self.agent {
4131 let agent = agent.clone();
4132 let tx = self.agent_tx.clone();
4133 let terminal = self
4134 .tab_manager
4135 .active_tab()
4136 .map(|tab| tab.terminal.clone());
4137 let cmd_for_msg = cmd.clone();
4138 self.runtime.spawn(async move {
4139 let mut exit_code: Option<i32> = None;
4141 for _ in 0..300 {
4142 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
4143 if let Some(ref terminal) = terminal
4144 && let Ok(term) = terminal.try_lock()
4145 {
4146 let history = term.core_command_history();
4147 if history.len() > history_len {
4148 if let Some(last) = history.last() {
4150 exit_code = last.1;
4151 }
4152 break;
4153 }
4154 }
4155 }
4156 let exit_str = exit_code
4158 .map(|c| format!("exit code {c}"))
4159 .unwrap_or_else(|| "unknown exit code".to_string());
4160 let feedback = format!(
4161 "[System: The user executed `{cmd_for_msg}` in their terminal ({exit_str}). \
4162 The output is available through the normal terminal capture.]"
4163 );
4164 let content = vec![par_term_acp::ContentBlock::Text {
4165 text: feedback,
4166 }];
4167 let agent = agent.lock().await;
4168 let _ = agent.send_prompt(content).await;
4169 if let Some(tx) = tx {
4170 let _ = tx.send(par_term_acp::AgentMessage::PromptComplete);
4171 }
4172 });
4173 }
4174 self.needs_redraw = true;
4175 }
4176 InspectorAction::ConnectAgent(identity) => {
4177 self.connect_agent(&identity);
4178 }
4179 InspectorAction::DisconnectAgent => {
4180 if let Some(agent) = self.agent.take() {
4181 self.runtime.spawn(async move {
4182 let mut agent = agent.lock().await;
4183 agent.disconnect().await;
4184 });
4185 }
4186 self.agent_rx = None;
4187 self.agent_tx = None;
4188 self.agent_client = None;
4189 self.ai_inspector.agent_status = AgentStatus::Disconnected;
4190 self.needs_redraw = true;
4191 }
4192 InspectorAction::SendPrompt(text) => {
4193 self.ai_inspector.chat.add_user_message(text.clone());
4194 if let Some(agent) = &self.agent {
4195 let agent = agent.clone();
4196 let mut prompt_text = String::new();
4198
4199 if !self.ai_inspector.chat.system_prompt_sent {
4202 self.ai_inspector.chat.system_prompt_sent = true;
4203 prompt_text.push_str(crate::ai_inspector::chat::AGENT_SYSTEM_GUIDANCE);
4204 }
4205
4206 if crate::ai_inspector::shader_context::should_inject_shader_context(
4208 &text,
4209 &self.config,
4210 ) {
4211 prompt_text.push_str(
4212 &crate::ai_inspector::shader_context::build_shader_context(
4213 &self.config,
4214 ),
4215 );
4216 }
4217
4218 prompt_text.push_str(&text);
4219
4220 let content = vec![par_term_acp::ContentBlock::Text { text: prompt_text }];
4221 let tx = self.agent_tx.clone();
4222 self.runtime.spawn(async move {
4223 let agent = agent.lock().await;
4224 let _ = agent.send_prompt(content).await;
4225 if let Some(tx) = tx {
4228 let _ = tx.send(AgentMessage::PromptComplete);
4229 }
4230 });
4231 }
4232 self.needs_redraw = true;
4233 }
4234 InspectorAction::SetTerminalAccess(enabled) => {
4235 self.config.ai_inspector_agent_terminal_access = enabled;
4236 self.needs_redraw = true;
4237 }
4238 InspectorAction::RespondPermission {
4239 request_id,
4240 option_id,
4241 cancelled,
4242 } => {
4243 if let Some(client) = &self.agent_client {
4244 let client = client.clone();
4245 let action = if cancelled { "cancelled" } else { "selected" };
4246 log::info!("ACP: sending permission response id={request_id} action={action}");
4247 self.runtime.spawn(async move {
4248 use par_term_acp::{PermissionOutcome, RequestPermissionResponse};
4249 let outcome = if cancelled {
4250 PermissionOutcome {
4251 outcome: "cancelled".to_string(),
4252 option_id: None,
4253 }
4254 } else {
4255 PermissionOutcome {
4256 outcome: "selected".to_string(),
4257 option_id: Some(option_id),
4258 }
4259 };
4260 let result = RequestPermissionResponse { outcome };
4261 if let Err(e) = client
4262 .respond(
4263 request_id,
4264 Some(serde_json::to_value(&result).unwrap()),
4265 None,
4266 )
4267 .await
4268 {
4269 log::error!("ACP: failed to send permission response: {e}");
4270 }
4271 });
4272 } else {
4273 log::error!(
4274 "ACP: cannot send permission response id={request_id} — agent_client is None!"
4275 );
4276 }
4277 for msg in &mut self.ai_inspector.chat.messages {
4279 if let ChatMessage::Permission {
4280 request_id: rid,
4281 resolved,
4282 ..
4283 } = msg
4284 && *rid == request_id
4285 {
4286 *resolved = true;
4287 break;
4288 }
4289 }
4290 self.needs_redraw = true;
4291 }
4292 InspectorAction::SetAgentMode(mode_id) => {
4293 let is_yolo = mode_id == "bypassPermissions";
4294 self.config.ai_inspector_auto_approve = is_yolo;
4295 if let Some(agent) = &self.agent {
4296 let agent = agent.clone();
4297 self.runtime.spawn(async move {
4298 let agent = agent.lock().await;
4299 agent
4300 .auto_approve
4301 .store(is_yolo, std::sync::atomic::Ordering::Relaxed);
4302 if let Err(e) = agent.set_mode(&mode_id).await {
4303 log::error!("ACP: failed to set mode '{mode_id}': {e}");
4304 }
4305 });
4306 }
4307 self.needs_redraw = true;
4308 }
4309 InspectorAction::None => {}
4310 }
4311
4312 match pending_session_picker_action {
4315 SessionPickerAction::Attach(session_name) => {
4316 crate::debug_info!(
4317 "TMUX",
4318 "Session picker: attaching to '{}' via gateway",
4319 session_name
4320 );
4321 if let Err(e) = self.attach_tmux_gateway(&session_name) {
4322 log::error!("Failed to attach to tmux session '{}': {}", session_name, e);
4323 self.show_toast(format!("Failed to attach: {}", e));
4324 } else {
4325 crate::debug_info!("TMUX", "Gateway initiated for session '{}'", session_name);
4326 self.show_toast(format!("Connecting to session '{}'...", session_name));
4327 }
4328 self.needs_redraw = true;
4329 }
4330 SessionPickerAction::CreateNew(name) => {
4331 crate::debug_info!(
4332 "TMUX",
4333 "Session picker: creating new session {:?} via gateway",
4334 name
4335 );
4336 if let Err(e) = self.initiate_tmux_gateway(name.as_deref()) {
4337 log::error!("Failed to create tmux session: {}", e);
4338 crate::debug_error!("TMUX", "Failed to initiate gateway: {}", e);
4339 self.show_toast(format!("Failed to create session: {}", e));
4340 } else {
4341 let msg = match name {
4342 Some(ref n) => format!("Creating session '{}'...", n),
4343 None => "Creating new tmux session...".to_string(),
4344 };
4345 crate::debug_info!("TMUX", "Gateway initiated: {}", msg);
4346 self.show_toast(msg);
4347 }
4348 self.needs_redraw = true;
4349 }
4350 SessionPickerAction::None => {}
4351 }
4352
4353 if let Some(ref rx) = self.shader_install_receiver
4355 && let Ok(result) = rx.try_recv()
4356 {
4357 match result {
4358 Ok(count) => {
4359 log::info!("Successfully installed {} shaders", count);
4360 self.shader_install_ui
4361 .set_success(&format!("Installed {} shaders!", count));
4362
4363 self.config.shader_install_prompt = ShaderInstallPrompt::Installed;
4365 if let Err(e) = self.config.save() {
4366 log::error!("Failed to save config after shader install: {}", e);
4367 }
4368 }
4369 Err(e) => {
4370 log::error!("Failed to install shaders: {}", e);
4371 self.shader_install_ui.set_error(&e);
4372 }
4373 }
4374 self.shader_install_receiver = None;
4375 self.needs_redraw = true;
4376 }
4377
4378 match pending_shader_install_response {
4380 ShaderInstallResponse::Install => {
4381 log::info!("User requested shader installation");
4382 self.shader_install_ui
4383 .set_installing("Downloading shaders...");
4384 self.needs_redraw = true;
4385
4386 let (tx, rx) = std::sync::mpsc::channel();
4388 self.shader_install_receiver = Some(rx);
4389
4390 std::thread::spawn(move || {
4391 let result = crate::shader_install_ui::install_shaders_headless();
4392 let _ = tx.send(result);
4393 });
4394
4395 if let Some(window) = &self.window {
4397 window.request_redraw();
4398 }
4399 }
4400 ShaderInstallResponse::Never => {
4401 log::info!("User declined shader installation (never ask again)");
4402 self.shader_install_ui.hide();
4403
4404 self.config.shader_install_prompt = ShaderInstallPrompt::Never;
4406 if let Err(e) = self.config.save() {
4407 log::error!("Failed to save config after declining shaders: {}", e);
4408 }
4409 }
4410 ShaderInstallResponse::Later => {
4411 log::info!("User deferred shader installation");
4412 self.shader_install_ui.hide();
4413 }
4415 ShaderInstallResponse::None => {}
4416 }
4417
4418 self.handle_integrations_response(&pending_integrations_response);
4420
4421 match pending_profile_drawer_action {
4423 ProfileDrawerAction::OpenProfile(id) => {
4424 self.open_profile(id);
4425 }
4426 ProfileDrawerAction::ManageProfiles => {
4427 self.open_settings_window_requested = true;
4429 self.open_settings_profiles_tab = true;
4430 }
4431 ProfileDrawerAction::None => {}
4432 }
4433
4434 let absolute_total = absolute_start.elapsed();
4435 if absolute_total.as_millis() > 10 {
4436 log::debug!(
4437 "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
4438 absolute_total.as_secs_f64() * 1000.0
4439 );
4440 }
4441 }
4442
4443 #[allow(clippy::too_many_arguments)]
4445 fn render_split_panes_with_data(
4446 renderer: &mut Renderer,
4447 pane_data: Vec<PaneRenderData>,
4448 dividers: Vec<crate::pane::DividerRect>,
4449 pane_titles: Vec<PaneTitleInfo>,
4450 focused_viewport: Option<PaneViewport>,
4451 config: &Config,
4452 egui_data: Option<(egui::FullOutput, &egui::Context)>,
4453 hovered_divider_index: Option<usize>,
4454 ) -> Result<bool> {
4455 let mut pane_render_infos: Vec<PaneRenderInfo> = Vec::new();
4457 let mut leaked_cells: Vec<*mut [crate::cell_renderer::Cell]> = Vec::new();
4458
4459 for (
4460 viewport,
4461 cells,
4462 grid_size,
4463 cursor_pos,
4464 cursor_opacity,
4465 marks,
4466 scrollback_len,
4467 scroll_offset,
4468 pane_background,
4469 ) in pane_data
4470 {
4471 let cells_boxed = cells.into_boxed_slice();
4472 let cells_ptr = Box::into_raw(cells_boxed);
4473 leaked_cells.push(cells_ptr);
4474
4475 pane_render_infos.push(PaneRenderInfo {
4476 viewport,
4477 cells: unsafe { &*cells_ptr },
4479 grid_size,
4480 cursor_pos,
4481 cursor_opacity,
4482 show_scrollbar: false,
4483 marks,
4484 scrollback_len,
4485 scroll_offset,
4486 background: pane_background,
4487 });
4488 }
4489
4490 let divider_render_infos: Vec<DividerRenderInfo> = dividers
4492 .iter()
4493 .enumerate()
4494 .map(|(i, d)| DividerRenderInfo::from_rect(d, hovered_divider_index == Some(i)))
4495 .collect();
4496
4497 let divider_settings = PaneDividerSettings {
4499 divider_color: [
4500 config.pane_divider_color[0] as f32 / 255.0,
4501 config.pane_divider_color[1] as f32 / 255.0,
4502 config.pane_divider_color[2] as f32 / 255.0,
4503 ],
4504 hover_color: [
4505 config.pane_divider_hover_color[0] as f32 / 255.0,
4506 config.pane_divider_hover_color[1] as f32 / 255.0,
4507 config.pane_divider_hover_color[2] as f32 / 255.0,
4508 ],
4509 show_focus_indicator: config.pane_focus_indicator,
4510 focus_color: [
4511 config.pane_focus_color[0] as f32 / 255.0,
4512 config.pane_focus_color[1] as f32 / 255.0,
4513 config.pane_focus_color[2] as f32 / 255.0,
4514 ],
4515 focus_width: config.pane_focus_width * renderer.scale_factor(),
4516 divider_style: config.pane_divider_style,
4517 };
4518
4519 let result = renderer.render_split_panes(
4521 &pane_render_infos,
4522 ÷r_render_infos,
4523 &pane_titles,
4524 focused_viewport.as_ref(),
4525 ÷r_settings,
4526 egui_data,
4527 false,
4528 );
4529
4530 for ptr in leaked_cells {
4532 let _ = unsafe { Box::from_raw(ptr) };
4534 }
4535
4536 result
4537 }
4538
4539 fn handle_integrations_response(&mut self, response: &IntegrationsResponse) {
4541 if !response.install_shaders
4543 && !response.install_shell_integration
4544 && !response.skipped
4545 && !response.never_ask
4546 && !response.closed
4547 && response.shader_conflict_action.is_none()
4548 {
4549 return;
4550 }
4551
4552 let current_version = env!("CARGO_PKG_VERSION").to_string();
4553
4554 let mut install_shaders = false;
4556 let mut install_shell_integration = false;
4557 let mut force_overwrite_modified_shaders = false;
4558 let mut triggered_install = false;
4559
4560 if let Some(action) = response.shader_conflict_action {
4562 triggered_install = true;
4563 install_shaders = self.integrations_ui.pending_install_shaders;
4564 install_shell_integration = self.integrations_ui.pending_install_shell_integration;
4565
4566 match action {
4567 crate::integrations_ui::ShaderConflictAction::Overwrite => {
4568 force_overwrite_modified_shaders = true;
4569 }
4570 crate::integrations_ui::ShaderConflictAction::SkipModified => {
4571 force_overwrite_modified_shaders = false;
4572 }
4573 crate::integrations_ui::ShaderConflictAction::Cancel => {
4574 self.integrations_ui.awaiting_shader_overwrite = false;
4576 self.integrations_ui.shader_conflicts.clear();
4577 self.integrations_ui.pending_install_shaders = false;
4578 self.integrations_ui.pending_install_shell_integration = false;
4579 self.integrations_ui.error_message = None;
4580 self.integrations_ui.success_message = None;
4581 self.needs_redraw = true;
4582 return;
4583 }
4584 }
4585
4586 self.integrations_ui.awaiting_shader_overwrite = false;
4588 self.integrations_ui.shader_conflicts.clear();
4589 self.integrations_ui.error_message = None;
4590 self.integrations_ui.success_message = None;
4591 self.integrations_ui.installing = false;
4592 } else if response.install_shaders || response.install_shell_integration {
4593 triggered_install = true;
4594 install_shaders = response.install_shaders;
4595 install_shell_integration = response.install_shell_integration;
4596
4597 if install_shaders {
4598 match crate::shader_installer::detect_modified_bundled_shaders() {
4599 Ok(conflicts) if !conflicts.is_empty() => {
4600 log::info!(
4601 "Detected {} modified bundled shaders; prompting for overwrite",
4602 conflicts.len()
4603 );
4604 self.integrations_ui.awaiting_shader_overwrite = true;
4605 self.integrations_ui.shader_conflicts = conflicts;
4606 self.integrations_ui.pending_install_shaders = install_shaders;
4607 self.integrations_ui.pending_install_shell_integration =
4608 install_shell_integration;
4609 self.integrations_ui.installing = false;
4610 self.integrations_ui.error_message = None;
4611 self.integrations_ui.success_message = None;
4612 self.needs_redraw = true;
4613 return; }
4615 Ok(_) => {}
4616 Err(e) => {
4617 log::warn!(
4618 "Unable to check existing shaders for modifications: {}. Proceeding without overwrite prompt.",
4619 e
4620 );
4621 }
4622 }
4623 }
4624 }
4625
4626 if triggered_install {
4628 log::info!(
4629 "User requested installations: shaders={}, shell_integration={}, overwrite_modified={}",
4630 install_shaders,
4631 install_shell_integration,
4632 force_overwrite_modified_shaders
4633 );
4634
4635 let mut success_parts = Vec::new();
4636 let mut error_parts = Vec::new();
4637
4638 if install_shaders {
4640 self.integrations_ui.set_installing("Installing shaders...");
4641 self.needs_redraw = true;
4642 self.request_redraw();
4643
4644 match crate::shader_installer::install_shaders_with_manifest(
4645 force_overwrite_modified_shaders,
4646 ) {
4647 Ok(result) => {
4648 log::info!(
4649 "Installed {} shader files ({} skipped, {} removed)",
4650 result.installed,
4651 result.skipped,
4652 result.removed
4653 );
4654 let detail = if result.skipped > 0 {
4655 format!("{} shaders ({} skipped)", result.installed, result.skipped)
4656 } else {
4657 format!("{} shaders", result.installed)
4658 };
4659 success_parts.push(detail);
4660 self.config.integration_versions.shaders_installed_version =
4661 Some(current_version.clone());
4662 self.config.integration_versions.shaders_prompted_version =
4663 Some(current_version.clone());
4664 }
4665 Err(e) => {
4666 log::error!("Failed to install shaders: {}", e);
4667 error_parts.push(format!("Shaders: {}", e));
4668 }
4669 }
4670 }
4671
4672 if install_shell_integration {
4674 self.integrations_ui
4675 .set_installing("Installing shell integration...");
4676 self.needs_redraw = true;
4677 self.request_redraw();
4678
4679 match crate::shell_integration_installer::install(None) {
4680 Ok(result) => {
4681 log::info!(
4682 "Installed shell integration for {}",
4683 result.shell.display_name()
4684 );
4685 success_parts.push(format!(
4686 "shell integration ({})",
4687 result.shell.display_name()
4688 ));
4689 self.config
4690 .integration_versions
4691 .shell_integration_installed_version = Some(current_version.clone());
4692 self.config
4693 .integration_versions
4694 .shell_integration_prompted_version = Some(current_version.clone());
4695 }
4696 Err(e) => {
4697 log::error!("Failed to install shell integration: {}", e);
4698 error_parts.push(format!("Shell: {}", e));
4699 }
4700 }
4701 }
4702
4703 if error_parts.is_empty() {
4705 self.integrations_ui
4706 .set_success(&format!("Installed: {}", success_parts.join(", ")));
4707 } else if success_parts.is_empty() {
4708 self.integrations_ui
4709 .set_error(&format!("Installation failed: {}", error_parts.join("; ")));
4710 } else {
4711 self.integrations_ui.set_success(&format!(
4713 "Installed: {}. Errors: {}",
4714 success_parts.join(", "),
4715 error_parts.join("; ")
4716 ));
4717 }
4718
4719 if let Err(e) = self.config.save() {
4721 log::error!("Failed to save config after integration install: {}", e);
4722 }
4723
4724 self.integrations_ui.pending_install_shaders = false;
4726 self.integrations_ui.pending_install_shell_integration = false;
4727
4728 self.needs_redraw = true;
4729 }
4730
4731 if response.skipped {
4733 log::info!("User skipped integrations dialog for this session");
4734 self.integrations_ui.hide();
4735 self.config.integration_versions.shaders_prompted_version =
4737 Some(current_version.clone());
4738 self.config
4739 .integration_versions
4740 .shell_integration_prompted_version = Some(current_version.clone());
4741 if let Err(e) = self.config.save() {
4742 log::error!("Failed to save config after skipping integrations: {}", e);
4743 }
4744 }
4745
4746 if response.never_ask {
4748 log::info!("User declined integrations (never ask again)");
4749 self.integrations_ui.hide();
4750 self.config.shader_install_prompt = ShaderInstallPrompt::Never;
4752 self.config.shell_integration_state = crate::config::InstallPromptState::Never;
4753 if let Err(e) = self.config.save() {
4754 log::error!("Failed to save config after declining integrations: {}", e);
4755 }
4756 }
4757
4758 if response.closed {
4760 self.integrations_ui.hide();
4761 }
4762 }
4763
4764 pub(crate) fn perform_shutdown(&mut self) {
4766 if self.config.startup_directory_mode == crate::config::StartupDirectoryMode::Previous
4768 && let Some(tab) = self.tab_manager.active_tab()
4769 && let Ok(term) = tab.terminal.try_lock()
4770 && let Some(cwd) = term.shell_integration_cwd()
4771 {
4772 log::info!("Saving last working directory: {}", cwd);
4773 if let Err(e) = self.config.save_last_working_directory(&cwd) {
4774 log::warn!("Failed to save last working directory: {}", e);
4775 }
4776 }
4777
4778 self.is_shutting_down = true;
4780 for tab in self.tab_manager.tabs_mut() {
4782 if let Some(task) = tab.refresh_task.take() {
4783 task.abort();
4784 }
4785 }
4786 log::info!("Refresh tasks aborted, shutdown initiated");
4787 }
4788}
4789
4790impl Drop for WindowState {
4792 fn drop(&mut self) {
4793 let t0 = std::time::Instant::now();
4794 log::info!("Shutting down window (fast path)");
4795
4796 self.status_bar_ui.signal_shutdown();
4800
4801 self.command_history.save_background();
4803
4804 self.is_shutting_down = true;
4806
4807 if let Some(ref window) = self.window {
4809 window.set_visible(false);
4810 log::info!(
4811 "Window hidden for instant visual close (+{:.1}ms)",
4812 t0.elapsed().as_secs_f64() * 1000.0
4813 );
4814 }
4815
4816 self.egui_state = None;
4818 self.egui_ctx = None;
4819
4820 let mut tabs = self.tab_manager.drain_tabs();
4822 let tab_count = tabs.len();
4823 log::info!(
4824 "Fast shutdown: draining {} tabs (+{:.1}ms)",
4825 tab_count,
4826 t0.elapsed().as_secs_f64() * 1000.0
4827 );
4828
4829 let mut terminal_arcs = Vec::new();
4834 let mut session_loggers = Vec::new();
4835
4836 for tab in &mut tabs {
4837 tab.stop_refresh_task();
4839
4840 session_loggers.push(Arc::clone(&tab.session_logger));
4842
4843 terminal_arcs.push(Arc::clone(&tab.terminal));
4845
4846 if let Some(ref mut pm) = tab.pane_manager {
4848 for pane in pm.all_panes_mut() {
4849 pane.stop_refresh_task();
4850 session_loggers.push(Arc::clone(&pane.session_logger));
4851 terminal_arcs.push(Arc::clone(&pane.terminal));
4852 pane.shutdown_fast = true;
4853 }
4854 }
4855
4856 tab.shutdown_fast = true;
4858 }
4859
4860 for arc in &terminal_arcs {
4862 if let Ok(mut term) = arc.try_lock()
4863 && term.is_running()
4864 {
4865 let _ = term.kill();
4866 }
4867 }
4868 log::info!(
4869 "Pre-killed {} terminal sessions (+{:.1}ms)",
4870 terminal_arcs.len(),
4871 t0.elapsed().as_secs_f64() * 1000.0
4872 );
4873
4874 drop(tabs);
4876 log::info!(
4877 "Tabs dropped (+{:.1}ms)",
4878 t0.elapsed().as_secs_f64() * 1000.0
4879 );
4880
4881 if !session_loggers.is_empty() {
4884 let _ = std::thread::Builder::new()
4885 .name("logger-cleanup".into())
4886 .spawn(move || {
4887 for logger_arc in session_loggers {
4888 if let Some(ref mut logger) = *logger_arc.lock() {
4889 let _ = logger.stop();
4890 }
4891 }
4892 });
4893 }
4894
4895 for (i, arc) in terminal_arcs.into_iter().enumerate() {
4902 let _ = std::thread::Builder::new()
4903 .name(format!("pty-cleanup-{}", i))
4904 .spawn(move || {
4905 let t = std::time::Instant::now();
4906 drop(arc);
4907 log::info!(
4908 "pty-cleanup-{} finished in {:.1}ms",
4909 i,
4910 t.elapsed().as_secs_f64() * 1000.0
4911 );
4912 });
4913 }
4914
4915 log::info!(
4916 "Window shutdown complete ({} tabs, main thread blocked {:.1}ms)",
4917 tab_count,
4918 t0.elapsed().as_secs_f64() * 1000.0
4919 );
4920 }
4921}