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
319fn json_as_f32(value: &serde_json::Value) -> Result<f32, String> {
321 if let Some(f) = value.as_f64() {
322 Ok(f as f32)
323 } else if let Some(i) = value.as_i64() {
324 Ok(i as f32)
325 } else {
326 Err("expected number".to_string())
327 }
328}
329
330impl WindowState {
331 pub fn new(config: Config, runtime: Arc<Runtime>) -> Self {
333 let keybinding_registry = KeybindingRegistry::from_config(&config.keybindings);
334 let shaders_dir = Config::shaders_dir();
335 let tmux_prefix_key = crate::tmux::PrefixKey::parse(&config.tmux_prefix_key);
336
337 let mut input_handler = InputHandler::new();
338 input_handler
340 .update_option_key_modes(config.left_option_key_mode, config.right_option_key_mode);
341
342 let profile_manager = match profile_storage::load_profiles() {
344 Ok(manager) => manager,
345 Err(e) => {
346 log::warn!("Failed to load profiles: {}", e);
347 ProfileManager::new()
348 }
349 };
350
351 let badge_state = BadgeState::new(&config);
353 let ai_inspector = AIInspectorPanel::new(&config);
354
355 let config_dir = dirs::config_dir().unwrap_or_default().join("par-term");
357 let available_agents = discover_agents(&config_dir);
358 let command_history_max = config.command_history_max_entries;
359
360 Self {
361 config,
362 window: None,
363 renderer: None,
364 input_handler,
365 runtime,
366
367 tab_manager: TabManager::new(),
368 tab_bar_ui: TabBarUI::new(),
369 tmux_status_bar_ui: TmuxStatusBarUI::new(),
370 status_bar_ui: StatusBarUI::new(),
371
372 debug: DebugState::new(),
373
374 cursor_opacity: 1.0,
375 last_cursor_blink: None,
376 last_key_press: None,
377 is_fullscreen: false,
378 egui_ctx: None,
379 egui_state: None,
380 pending_egui_events: Vec::new(),
381 egui_initialized: false,
382 shader_metadata_cache: ShaderMetadataCache::with_shaders_dir(shaders_dir.clone()),
383 cursor_shader_metadata_cache: CursorShaderMetadataCache::with_shaders_dir(shaders_dir),
384 help_ui: HelpUI::new(),
385 clipboard_history_ui: ClipboardHistoryUI::new(),
386 command_history_ui: CommandHistoryUI::new(),
387 command_history: {
388 let mut ch = CommandHistory::new(command_history_max);
389 ch.load();
390 ch
391 },
392 synced_commands: std::collections::HashSet::new(),
393 paste_special_ui: PasteSpecialUI::new(),
394 tmux_session_picker_ui: TmuxSessionPickerUI::new(),
395 search_ui: SearchUI::new(),
396 ai_inspector,
397 last_inspector_width: 0.0,
398 agent_rx: None,
399 agent_tx: None,
400 agent: None,
401 agent_client: None,
402 available_agents,
403 shader_install_ui: ShaderInstallUI::new(),
404 shader_install_receiver: None,
405 integrations_ui: IntegrationsUI::new(),
406 close_confirmation_ui: CloseConfirmationUI::new(),
407 quit_confirmation_ui: QuitConfirmationUI::new(),
408 remote_shell_install_ui: RemoteShellInstallUI::new(),
409 ssh_connect_ui: SshConnectUI::new(),
410 is_recording: false,
411 recording_start_time: None,
412 is_shutting_down: false,
413 window_index: 1, needs_redraw: true,
416 config_changed_by_agent: false,
417 cursor_blink_timer: None,
418 pending_font_rebuild: false,
419
420 is_focused: true, last_render_time: None,
422
423 cursor_hidden_since: None,
424 flicker_pending_render: false,
425
426 throughput_batch_start: None,
427
428 shader_watcher: None,
429 config_watcher: None,
430 config_update_watcher: None,
431 shader_reload_error: None,
432 background_shader_reload_result: None,
433 cursor_shader_reload_result: None,
434
435 open_settings_window_requested: false,
436 pending_arrangement_restore: None,
437 reload_dynamic_profiles_requested: false,
438
439 profile_manager,
440 profile_drawer_ui: ProfileDrawerUI::new(),
441 open_settings_profiles_tab: false,
442 profiles_menu_needs_update: true, ui_consumed_mouse_press: false,
444 focus_click_pending: false,
445
446 resize_overlay_visible: false,
447 resize_overlay_hide_time: None,
448 resize_dimensions: None,
449
450 toast_message: None,
451 toast_hide_time: None,
452 pane_identify_hide_time: None,
453 closed_tabs: std::collections::VecDeque::new(),
454
455 keybinding_registry,
456
457 smart_selection_cache: SmartSelectionCache::new(),
458
459 tmux_session: None,
460 tmux_sync: TmuxSync::new(),
461 tmux_session_name: None,
462 tmux_gateway_tab_id: None,
463 tmux_prefix_key,
464 tmux_prefix_state: crate::tmux::PrefixState::new(),
465 tmux_pane_to_native_pane: std::collections::HashMap::new(),
466 native_pane_to_tmux_pane: std::collections::HashMap::new(),
467
468 broadcast_input: false,
469
470 badge_state,
471
472 copy_mode: crate::copy_mode::CopyModeState::new(),
473
474 file_transfer_state: crate::app::file_transfers::FileTransferState::default(),
475 }
476 }
477
478 pub(crate) fn format_title(&self, base_title: &str) -> String {
481 if self.config.show_window_number {
482 format!("{} [{}]", base_title, self.window_index)
483 } else {
484 base_title.to_string()
485 }
486 }
487
488 #[allow(dead_code)]
492 pub(crate) fn terminal(
493 &self,
494 ) -> Option<&Arc<tokio::sync::Mutex<crate::terminal::TerminalManager>>> {
495 self.active_terminal()
496 }
497
498 #[allow(dead_code)]
499 pub(crate) fn scroll_state(&self) -> Option<&crate::scroll_state::ScrollState> {
500 self.tab_manager.active_tab().map(|t| &t.scroll_state)
501 }
502
503 #[allow(dead_code)]
504 pub(crate) fn scroll_state_mut(&mut self) -> Option<&mut crate::scroll_state::ScrollState> {
505 self.tab_manager
506 .active_tab_mut()
507 .map(|t| &mut t.scroll_state)
508 }
509
510 #[allow(dead_code)]
511 pub(crate) fn mouse(&self) -> Option<&crate::app::mouse::MouseState> {
512 self.tab_manager.active_tab().map(|t| &t.mouse)
513 }
514
515 #[allow(dead_code)]
516 pub(crate) fn mouse_mut(&mut self) -> Option<&mut crate::app::mouse::MouseState> {
517 self.tab_manager.active_tab_mut().map(|t| &mut t.mouse)
518 }
519
520 #[allow(dead_code)]
521 pub(crate) fn bell(&self) -> Option<&crate::app::bell::BellState> {
522 self.tab_manager.active_tab().map(|t| &t.bell)
523 }
524
525 #[allow(dead_code)]
526 pub(crate) fn bell_mut(&mut self) -> Option<&mut crate::app::bell::BellState> {
527 self.tab_manager.active_tab_mut().map(|t| &mut t.bell)
528 }
529
530 #[allow(dead_code)]
531 pub(crate) fn cache(&self) -> Option<&crate::app::render_cache::RenderCache> {
532 self.tab_manager.active_tab().map(|t| &t.cache)
533 }
534
535 #[allow(dead_code)]
536 pub(crate) fn cache_mut(&mut self) -> Option<&mut crate::app::render_cache::RenderCache> {
537 self.tab_manager.active_tab_mut().map(|t| &mut t.cache)
538 }
539
540 #[allow(dead_code)]
541 pub(crate) fn refresh_task(&self) -> Option<&Option<tokio::task::JoinHandle<()>>> {
542 self.tab_manager.active_tab().map(|t| &t.refresh_task)
543 }
544
545 #[allow(dead_code)]
546 pub(crate) fn abort_refresh_task(&mut self) {
547 if let Some(tab) = self.tab_manager.active_tab_mut()
548 && let Some(task) = tab.refresh_task.take()
549 {
550 task.abort();
551 }
552 }
553
554 pub(crate) fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
556 let mut extracted = String::new();
557 let end_bound = end_col.unwrap_or(usize::MAX);
558
559 if start_col > end_bound {
560 return extracted;
561 }
562
563 for (idx, ch) in line.chars().enumerate() {
564 if idx > end_bound {
565 break;
566 }
567
568 if idx >= start_col {
569 extracted.push(ch);
570 }
571 }
572
573 extracted
574 }
575
576 #[inline]
582 pub(crate) fn invalidate_tab_cache(&mut self) {
583 if let Some(tab) = self.tab_manager.active_tab_mut() {
584 tab.cache.cells = None;
585 }
586 }
587
588 #[inline]
590 pub(crate) fn request_redraw(&self) {
591 if let Some(window) = &self.window {
592 crate::debug_trace!("REDRAW", "request_redraw called");
593 window.request_redraw();
594 } else {
595 crate::debug_trace!("REDRAW", "request_redraw called but no window");
596 }
597 }
598
599 #[inline]
601 #[allow(dead_code)] pub(crate) fn invalidate_and_redraw(&mut self) {
603 self.invalidate_tab_cache();
604 self.needs_redraw = true;
605 self.request_redraw();
606 }
607
608 pub(crate) fn clear_and_invalidate(&mut self) {
610 if let Some(renderer) = &mut self.renderer {
611 renderer.clear_all_cells();
612 }
613 self.invalidate_tab_cache();
614 self.needs_redraw = true;
615 self.request_redraw();
616 }
617
618 pub(crate) fn rebuild_renderer(&mut self) -> Result<()> {
620 use crate::app::renderer_init::RendererInitParams;
621
622 let window = if let Some(w) = &self.window {
623 Arc::clone(w)
624 } else {
625 return Ok(()); };
627
628 let theme = self.config.load_theme();
630 let metadata = self
632 .config
633 .custom_shader
634 .as_ref()
635 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
636 let cursor_metadata = self
638 .config
639 .cursor_shader
640 .as_ref()
641 .and_then(|name| self.cursor_shader_metadata_cache.get(name).cloned());
642 let params = RendererInitParams::from_config(
643 &self.config,
644 &theme,
645 metadata.as_ref(),
646 cursor_metadata.as_ref(),
647 );
648
649 self.renderer = None;
653
654 let mut renderer = self
655 .runtime
656 .block_on(params.create_renderer(Arc::clone(&window)))?;
657
658 let (cols, rows) = renderer.grid_size();
659 let cell_width = renderer.cell_width();
660 let cell_height = renderer.cell_height();
661 let width_px = (cols as f32 * cell_width) as usize;
662 let height_px = (rows as f32 * cell_height) as usize;
663
664 for tab in self.tab_manager.tabs_mut() {
666 if let Ok(mut term) = tab.terminal.try_lock() {
667 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
668 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
669 term.set_theme(self.config.load_theme());
670 }
671 tab.cache.cells = None;
672 }
673
674 self.apply_cursor_shader_config(&mut renderer, ¶ms);
676
677 self.renderer = Some(renderer);
678 self.needs_redraw = true;
679
680 self.last_inspector_width = 0.0;
684 self.sync_ai_inspector_width();
685
686 self.init_egui(&window, true);
688 self.request_redraw();
689
690 Ok(())
691 }
692
693 pub(crate) async fn initialize_async(&mut self, window: Window) -> Result<()> {
695 use crate::app::renderer_init::RendererInitParams;
696
697 window.set_ime_allowed(true);
699 log::debug!("IME enabled for character input");
700
701 if self.config.auto_dark_mode {
703 let is_dark = window
704 .theme()
705 .is_none_or(|t| t == winit::window::Theme::Dark);
706 if self.config.apply_system_theme(is_dark) {
707 log::info!(
708 "Auto dark mode: detected {} system theme, using theme: {}",
709 if is_dark { "dark" } else { "light" },
710 self.config.theme
711 );
712 }
713 }
714
715 {
717 let is_dark = window
718 .theme()
719 .is_none_or(|t| t == winit::window::Theme::Dark);
720 if self.config.apply_system_tab_style(is_dark) {
721 log::info!(
722 "Auto tab style: detected {} system theme, applying {} tab style",
723 if is_dark { "dark" } else { "light" },
724 if is_dark {
725 self.config.dark_tab_style.display_name()
726 } else {
727 self.config.light_tab_style.display_name()
728 }
729 );
730 }
731 }
732
733 let window = Arc::new(window);
734
735 self.init_egui(&window, false);
737
738 let theme = self.config.load_theme();
740 let metadata = self
742 .config
743 .custom_shader
744 .as_ref()
745 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
746 let cursor_metadata = self
748 .config
749 .cursor_shader
750 .as_ref()
751 .and_then(|name| self.cursor_shader_metadata_cache.get(name).cloned());
752 let params = RendererInitParams::from_config(
753 &self.config,
754 &theme,
755 metadata.as_ref(),
756 cursor_metadata.as_ref(),
757 );
758 let mut renderer = params.create_renderer(Arc::clone(&window)).await?;
759
760 #[cfg(target_os = "macos")]
764 {
765 if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
766 log::warn!("Failed to configure Metal layer: {}", e);
767 log::warn!(
768 "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
769 );
770 }
771 if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
773 log::warn!("Failed to set initial Metal layer opacity: {}", e);
774 }
775 if self.config.blur_enabled
777 && self.config.window_opacity < 1.0
778 && let Err(e) = crate::macos_blur::set_window_blur(&window, self.config.blur_radius)
779 {
780 log::warn!("Failed to set initial window blur: {}", e);
781 }
782 }
783
784 self.apply_cursor_shader_config(&mut renderer, ¶ms);
786
787 let initial_tab_bar_height = self.tab_bar_ui.get_height(1, &self.config);
791 let initial_tab_bar_width = self.tab_bar_ui.get_width(1, &self.config);
792 let (initial_cols, initial_rows) = renderer.grid_size();
793 log::info!(
794 "Tab bar init: mode={:?}, position={:?}, height={:.1}, width={:.1}, initial_grid={}x{}, content_offset_y_before={:.1}",
795 self.config.tab_bar_mode,
796 self.config.tab_bar_position,
797 initial_tab_bar_height,
798 initial_tab_bar_width,
799 initial_cols,
800 initial_rows,
801 renderer.content_offset_y()
802 );
803 self.apply_tab_bar_offsets(&mut renderer, initial_tab_bar_height, initial_tab_bar_width);
804
805 let (renderer_cols, renderer_rows) = renderer.grid_size();
808 let cell_width = renderer.cell_width();
809 let cell_height = renderer.cell_height();
810
811 self.window = Some(Arc::clone(&window));
812 self.renderer = Some(renderer);
813
814 self.init_shader_watcher();
816
817 self.init_config_watcher();
819
820 self.init_config_update_watcher();
822
823 self.status_bar_ui.sync_monitor_state(&self.config);
825
826 log::info!(
829 "Creating first tab with grid size {}x{} (accounting for tab bar)",
830 renderer_cols,
831 renderer_rows
832 );
833 let tab_id = self.tab_manager.new_tab(
834 &self.config,
835 Arc::clone(&self.runtime),
836 false, Some((renderer_cols, renderer_rows)), )?;
839
840 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
842 let width_px = (renderer_cols as f32 * cell_width) as usize;
843 let height_px = (renderer_rows as f32 * cell_height) as usize;
844
845 if let Ok(mut term) = tab.terminal.try_lock() {
846 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
847 let _ = term.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px);
849 log::info!(
850 "Initial terminal dimensions: {}x{} ({}x{} px)",
851 renderer_cols,
852 renderer_rows,
853 width_px,
854 height_px
855 );
856 }
857
858 tab.start_refresh_task(
860 Arc::clone(&self.runtime),
861 Arc::clone(&window),
862 self.config.max_fps,
863 );
864 }
865
866 if self.ai_inspector.open {
868 self.try_auto_connect_agent();
869 }
870
871 if self.config.should_prompt_integrations() {
873 log::info!("Integrations not installed - showing welcome dialog");
874 self.integrations_ui.show_dialog();
875 self.needs_redraw = true;
876 window.request_redraw();
877 }
878
879 Ok(())
880 }
881
882 pub(crate) fn force_surface_reconfigure(&mut self) {
886 log::info!("Force surface reconfigure triggered");
887
888 if let Some(renderer) = &mut self.renderer {
889 renderer.reconfigure_surface();
891
892 renderer.clear_glyph_cache();
894
895 if let Some(tab) = self.tab_manager.active_tab_mut() {
897 tab.cache.cells = None;
898 }
899 }
900
901 #[cfg(target_os = "macos")]
903 {
904 if let Some(window) = &self.window
905 && let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(window)
906 {
907 log::warn!("Failed to reconfigure Metal layer: {}", e);
908 }
909 }
910
911 self.needs_redraw = true;
913 self.request_redraw();
914 }
915
916 pub(crate) fn apply_tab_bar_offsets(
924 &self,
925 renderer: &mut crate::renderer::Renderer,
926 tab_bar_height: f32,
927 tab_bar_width: f32,
928 ) -> Option<(usize, usize)> {
929 Self::apply_tab_bar_offsets_for_position(
930 self.config.tab_bar_position,
931 renderer,
932 tab_bar_height,
933 tab_bar_width,
934 )
935 }
936
937 pub(crate) fn apply_tab_bar_offsets_for_position(
939 position: crate::config::TabBarPosition,
940 renderer: &mut crate::renderer::Renderer,
941 tab_bar_height: f32,
942 tab_bar_width: f32,
943 ) -> Option<(usize, usize)> {
944 use crate::config::TabBarPosition;
945 let (offset_y, offset_x, inset_bottom) = match position {
946 TabBarPosition::Top => (tab_bar_height, 0.0, 0.0),
947 TabBarPosition::Bottom => (0.0, 0.0, tab_bar_height),
948 TabBarPosition::Left => (0.0, tab_bar_width, 0.0),
949 };
950
951 let mut result = None;
952 if let Some(grid) = renderer.set_content_offset_y(offset_y) {
953 result = Some(grid);
954 }
955 if let Some(grid) = renderer.set_content_offset_x(offset_x) {
956 result = Some(grid);
957 }
958 if let Some(grid) = renderer.set_content_inset_bottom(inset_bottom) {
959 result = Some(grid);
960 }
961 result
962 }
963
964 pub(crate) fn sync_ai_inspector_width(&mut self) {
974 let current_width = self.ai_inspector.consumed_width();
975
976 if let Some(renderer) = &mut self.renderer {
977 if let Some((new_cols, new_rows)) = renderer.set_content_inset_right(current_width) {
984 let cell_width = renderer.cell_width();
985 let cell_height = renderer.cell_height();
986 let width_px = (new_cols as f32 * cell_width) as usize;
987 let height_px = (new_rows as f32 * cell_height) as usize;
988
989 for tab in self.tab_manager.tabs_mut() {
990 if let Ok(mut term) = tab.terminal.try_lock() {
991 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
992 let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
993 }
994 tab.cache.cells = None;
995 }
996
997 crate::debug_info!(
998 "AI_INSPECTOR",
999 "Panel width synced to {:.0}px, resized terminals to {}x{}",
1000 current_width,
1001 new_cols,
1002 new_rows
1003 );
1004 self.needs_redraw = true;
1005 } else if (current_width - self.last_inspector_width).abs() >= 1.0 {
1006 self.needs_redraw = true;
1009 }
1010 }
1011
1012 if !self.ai_inspector.is_resizing()
1014 && (current_width - self.last_inspector_width).abs() >= 1.0
1015 && current_width > 0.0
1016 && self.ai_inspector.open
1017 {
1018 self.config.ai_inspector_width = self.ai_inspector.width;
1019 if let Err(e) = self.config.save() {
1021 log::error!("Failed to save AI inspector width: {}", e);
1022 }
1023 }
1024
1025 self.last_inspector_width = current_width;
1026 }
1027
1028 pub(crate) fn connect_agent(&mut self, identity: &str) {
1033 if let Some(agent_config) = self
1034 .available_agents
1035 .iter()
1036 .find(|a| a.identity == identity)
1037 {
1038 if let Some(old_agent) = self.agent.take() {
1040 let runtime = self.runtime.clone();
1041 runtime.spawn(async move {
1042 let mut agent = old_agent.lock().await;
1043 agent.disconnect().await;
1044 });
1045 }
1046 self.agent_rx = None;
1047 self.agent_tx = None;
1048 self.agent_client = None;
1049
1050 let (tx, rx) = mpsc::unbounded_channel();
1051 self.agent_rx = Some(rx);
1052 self.agent_tx = Some(tx.clone());
1053 let ui_tx = tx.clone();
1054 let safe_paths = SafePaths {
1055 config_dir: Config::config_dir(),
1056 shaders_dir: Config::shaders_dir(),
1057 };
1058 let mcp_server_bin =
1059 std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("par-term"));
1060 let agent = Agent::new(agent_config.clone(), tx, safe_paths, mcp_server_bin);
1061 agent.auto_approve.store(
1062 self.config.ai_inspector_auto_approve,
1063 std::sync::atomic::Ordering::Relaxed,
1064 );
1065 let agent = Arc::new(tokio::sync::Mutex::new(agent));
1066 self.agent = Some(agent.clone());
1067
1068 let fallback_cwd = std::env::current_dir()
1070 .unwrap_or_default()
1071 .to_string_lossy()
1072 .to_string();
1073 let cwd = if let Some(tab) = self.tab_manager.active_tab() {
1074 if let Ok(term) = tab.terminal.try_lock() {
1075 term.shell_integration_cwd()
1076 .unwrap_or_else(|| fallback_cwd.clone())
1077 } else {
1078 fallback_cwd.clone()
1079 }
1080 } else {
1081 fallback_cwd
1082 };
1083
1084 let capabilities = ClientCapabilities {
1085 fs: FsCapabilities {
1086 read_text_file: true,
1087 write_text_file: true,
1088 list_directory: true,
1089 find: true,
1090 },
1091 terminal: self.config.ai_inspector_agent_terminal_access,
1092 config: true,
1093 };
1094
1095 let auto_approve = self.config.ai_inspector_auto_approve;
1096 let runtime = self.runtime.clone();
1097 runtime.spawn(async move {
1098 let mut agent = agent.lock().await;
1099 if let Err(e) = agent.connect(&cwd, capabilities).await {
1100 log::error!("ACP: failed to connect to agent: {e}");
1101 return;
1102 }
1103 if let Some(client) = &agent.client {
1104 let _ = ui_tx.send(AgentMessage::ClientReady(Arc::clone(client)));
1105 }
1106 if auto_approve && let Err(e) = agent.set_mode("bypassPermissions").await {
1107 log::error!("ACP: failed to set bypassPermissions mode: {e}");
1108 }
1109 });
1110 }
1111 }
1112
1113 pub(crate) fn try_auto_connect_agent(&mut self) {
1115 if self.config.ai_inspector_auto_launch
1116 && self.ai_inspector.agent_status == AgentStatus::Disconnected
1117 && self.agent.is_none()
1118 {
1119 let identity = self.config.ai_inspector_agent.clone();
1120 if !identity.is_empty() {
1121 log::info!("ACP: auto-connecting to agent '{}'", identity);
1122 self.connect_agent(&identity);
1123 }
1124 }
1125 }
1126
1127 pub(crate) fn sync_status_bar_inset(&mut self) {
1137 let is_tmux = self.is_tmux_connected();
1138 let tmux_bar = crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux);
1139 let custom_bar = self.status_bar_ui.height(&self.config, self.is_fullscreen);
1140 let total = tmux_bar + custom_bar;
1141
1142 if let Some(renderer) = &mut self.renderer
1143 && let Some((new_cols, new_rows)) = renderer.set_egui_bottom_inset(total)
1144 {
1145 let cell_width = renderer.cell_width();
1146 let cell_height = renderer.cell_height();
1147 let width_px = (new_cols as f32 * cell_width) as usize;
1148 let height_px = (new_rows as f32 * cell_height) as usize;
1149
1150 for tab in self.tab_manager.tabs_mut() {
1151 if let Ok(mut term) = tab.terminal.try_lock() {
1152 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
1153 let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
1154 }
1155 tab.cache.cells = None;
1156 }
1157 }
1158 }
1159
1160 pub(crate) fn init_shader_watcher(&mut self) {
1165 debug_info!(
1166 "SHADER",
1167 "init_shader_watcher: hot_reload={}",
1168 self.config.shader_hot_reload
1169 );
1170
1171 if !self.config.shader_hot_reload {
1172 log::debug!("Shader hot reload disabled");
1173 return;
1174 }
1175
1176 let background_path = self
1177 .config
1178 .custom_shader
1179 .as_ref()
1180 .filter(|_| self.config.custom_shader_enabled)
1181 .map(|s| Config::shader_path(s));
1182
1183 let cursor_path = self
1184 .config
1185 .cursor_shader
1186 .as_ref()
1187 .filter(|_| self.config.cursor_shader_enabled)
1188 .map(|s| Config::shader_path(s));
1189
1190 debug_info!(
1191 "SHADER",
1192 "Shader paths: background={:?}, cursor={:?}",
1193 background_path,
1194 cursor_path
1195 );
1196
1197 if background_path.is_none() && cursor_path.is_none() {
1198 debug_info!("SHADER", "No shaders to watch for hot reload");
1199 return;
1200 }
1201
1202 match ShaderWatcher::new(
1203 background_path.as_deref(),
1204 cursor_path.as_deref(),
1205 self.config.shader_hot_reload_delay,
1206 ) {
1207 Ok(watcher) => {
1208 debug_info!(
1209 "SHADER",
1210 "Shader hot reload initialized (debounce: {}ms)",
1211 self.config.shader_hot_reload_delay
1212 );
1213 self.shader_watcher = Some(watcher);
1214 }
1215 Err(e) => {
1216 debug_info!("SHADER", "Failed to initialize shader hot reload: {}", e);
1217 }
1218 }
1219 }
1220
1221 pub(crate) fn reinit_shader_watcher(&mut self) {
1223 debug_info!(
1224 "SHADER",
1225 "reinit_shader_watcher CALLED: shader={:?}, cursor={:?}",
1226 self.config.custom_shader,
1227 self.config.cursor_shader
1228 );
1229 self.shader_watcher = None;
1231 self.shader_reload_error = None;
1232
1233 self.init_shader_watcher();
1235 }
1236
1237 pub(crate) fn init_config_watcher(&mut self) {
1242 let config_path = Config::config_path();
1243 if !config_path.exists() {
1244 debug_info!("CONFIG", "Config file does not exist, skipping watcher");
1245 return;
1246 }
1247 match crate::config::watcher::ConfigWatcher::new(&config_path, 500) {
1248 Ok(watcher) => {
1249 debug_info!("CONFIG", "Config watcher initialized");
1250 self.config_watcher = Some(watcher);
1251 }
1252 Err(e) => {
1253 debug_info!("CONFIG", "Failed to initialize config watcher: {}", e);
1254 }
1255 }
1256 }
1257
1258 pub(crate) fn init_config_update_watcher(&mut self) {
1263 let update_path = Config::config_dir().join(".config-update.json");
1264
1265 if !update_path.exists() {
1267 if let Some(parent) = update_path.parent() {
1268 let _ = std::fs::create_dir_all(parent);
1269 }
1270 let _ = std::fs::write(&update_path, "");
1271 }
1272
1273 match crate::config::watcher::ConfigWatcher::new(&update_path, 200) {
1274 Ok(watcher) => {
1275 debug_info!("CONFIG", "Config-update watcher initialized");
1276 self.config_update_watcher = Some(watcher);
1277 }
1278 Err(e) => {
1279 debug_info!(
1280 "CONFIG",
1281 "Failed to initialize config-update watcher: {}",
1282 e
1283 );
1284 }
1285 }
1286 }
1287
1288 pub(crate) fn check_config_update_file(&mut self) {
1293 let Some(watcher) = &self.config_update_watcher else {
1294 return;
1295 };
1296 if watcher.try_recv().is_none() {
1297 return;
1298 }
1299
1300 let update_path = Config::config_dir().join(".config-update.json");
1301 let content = match std::fs::read_to_string(&update_path) {
1302 Ok(c) if c.trim().is_empty() => return,
1303 Ok(c) => c,
1304 Err(e) => {
1305 log::warn!("CONFIG: failed to read config-update file: {e}");
1306 return;
1307 }
1308 };
1309
1310 match serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(&content)
1311 {
1312 Ok(updates) => {
1313 log::info!(
1314 "CONFIG: applying MCP config update ({} keys): {:?}",
1315 updates.len(),
1316 updates
1317 );
1318 if let Err(e) = self.apply_agent_config_updates(&updates) {
1319 log::error!("CONFIG: MCP config update failed: {e}");
1320 } else {
1321 self.config_changed_by_agent = true;
1322 }
1323 self.needs_redraw = true;
1324 }
1325 Err(e) => {
1326 log::error!("CONFIG: invalid JSON in config-update file: {e}");
1327 }
1328 }
1329
1330 let _ = std::fs::write(&update_path, "");
1332 }
1333
1334 pub(crate) fn check_config_reload(&mut self) {
1341 let Some(watcher) = &self.config_watcher else {
1342 return;
1343 };
1344 let Some(_event) = watcher.try_recv() else {
1345 return;
1346 };
1347
1348 log::info!("CONFIG: config file changed, reloading...");
1349
1350 match Config::load() {
1351 Ok(new_config) => {
1352 use crate::app::config_updates::ConfigChanges;
1353
1354 let changes = ConfigChanges::detect(&self.config, &new_config);
1355
1356 self.config = new_config;
1359
1360 log::info!(
1361 "CONFIG: shader_changed={} cursor_changed={} shader={:?}",
1362 changes.any_shader_change(),
1363 changes.any_cursor_shader_toggle(),
1364 self.config.custom_shader
1365 );
1366
1367 if let Some(renderer) = &mut self.renderer {
1369 if changes.any_shader_change() || changes.shader_per_shader_config {
1370 log::info!("CONFIG: applying background shader change to renderer");
1371 let shader_override = self
1372 .config
1373 .custom_shader
1374 .as_ref()
1375 .and_then(|name| self.config.shader_configs.get(name));
1376 let metadata = self
1377 .config
1378 .custom_shader
1379 .as_ref()
1380 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
1381 let resolved = crate::config::shader_config::resolve_shader_config(
1382 shader_override,
1383 metadata.as_ref(),
1384 &self.config,
1385 );
1386 if let Err(e) = renderer.set_custom_shader_enabled(
1387 self.config.custom_shader_enabled,
1388 self.config.custom_shader.as_deref(),
1389 self.config.window_opacity,
1390 self.config.custom_shader_animation,
1391 resolved.animation_speed,
1392 resolved.full_content,
1393 resolved.brightness,
1394 &resolved.channel_paths(),
1395 resolved.cubemap_path().map(|p| p.as_path()),
1396 ) {
1397 log::error!("Config reload: shader load failed: {e}");
1398 }
1399 }
1400 if changes.any_cursor_shader_toggle() {
1401 log::info!("CONFIG: applying cursor shader change to renderer");
1402 if let Err(e) = renderer.set_cursor_shader_enabled(
1403 self.config.cursor_shader_enabled,
1404 self.config.cursor_shader.as_deref(),
1405 self.config.window_opacity,
1406 self.config.cursor_shader_animation,
1407 self.config.cursor_shader_animation_speed,
1408 ) {
1409 log::error!("Config reload: cursor shader load failed: {e}");
1410 }
1411 }
1412 }
1413
1414 if changes.needs_watcher_reinit() {
1416 self.reinit_shader_watcher();
1417 }
1418
1419 self.needs_redraw = true;
1420 debug_info!("CONFIG", "Config reloaded successfully");
1421 }
1422 Err(e) => {
1423 log::error!("Failed to reload config: {}", e);
1424 }
1425 }
1426 }
1427
1428 fn apply_agent_config_updates(
1433 &mut self,
1434 updates: &std::collections::HashMap<String, serde_json::Value>,
1435 ) -> Result<(), String> {
1436 let mut errors = Vec::new();
1437 let old_config = self.config.clone();
1438
1439 for (key, value) in updates {
1440 if let Err(e) = self.apply_single_config_update(key, value) {
1441 errors.push(format!("{key}: {e}"));
1442 }
1443 }
1444
1445 if !errors.is_empty() {
1446 return Err(errors.join("; "));
1447 }
1448
1449 use crate::app::config_updates::ConfigChanges;
1451 let changes = ConfigChanges::detect(&old_config, &self.config);
1452
1453 log::info!(
1454 "ACP config/update: shader_change={} cursor_change={} old_shader={:?} new_shader={:?}",
1455 changes.any_shader_change(),
1456 changes.any_cursor_shader_toggle(),
1457 old_config.custom_shader,
1458 self.config.custom_shader
1459 );
1460
1461 if let Some(renderer) = &mut self.renderer {
1462 if changes.any_shader_change() || changes.shader_per_shader_config {
1463 log::info!("ACP config/update: applying background shader change to renderer");
1464 let shader_override = self
1465 .config
1466 .custom_shader
1467 .as_ref()
1468 .and_then(|name| self.config.shader_configs.get(name));
1469 let metadata = self
1470 .config
1471 .custom_shader
1472 .as_ref()
1473 .and_then(|name| self.shader_metadata_cache.get(name).cloned());
1474 let resolved = crate::config::shader_config::resolve_shader_config(
1475 shader_override,
1476 metadata.as_ref(),
1477 &self.config,
1478 );
1479 if let Err(e) = renderer.set_custom_shader_enabled(
1480 self.config.custom_shader_enabled,
1481 self.config.custom_shader.as_deref(),
1482 self.config.window_opacity,
1483 self.config.custom_shader_animation,
1484 resolved.animation_speed,
1485 resolved.full_content,
1486 resolved.brightness,
1487 &resolved.channel_paths(),
1488 resolved.cubemap_path().map(|p| p.as_path()),
1489 ) {
1490 log::error!("ACP config/update: shader load failed: {e}");
1491 }
1492 }
1493 if changes.any_cursor_shader_toggle() {
1494 log::info!("ACP config/update: applying cursor shader change to renderer");
1495 if let Err(e) = renderer.set_cursor_shader_enabled(
1496 self.config.cursor_shader_enabled,
1497 self.config.cursor_shader.as_deref(),
1498 self.config.window_opacity,
1499 self.config.cursor_shader_animation,
1500 self.config.cursor_shader_animation_speed,
1501 ) {
1502 log::error!("ACP config/update: cursor shader load failed: {e}");
1503 }
1504 }
1505 }
1506
1507 if changes.needs_watcher_reinit() {
1508 self.reinit_shader_watcher();
1509 }
1510
1511 if let Err(e) = self.config.save() {
1513 return Err(format!("Failed to save config: {e}"));
1514 }
1515
1516 Ok(())
1517 }
1518
1519 fn apply_single_config_update(
1521 &mut self,
1522 key: &str,
1523 value: &serde_json::Value,
1524 ) -> Result<(), String> {
1525 match key {
1526 "custom_shader" => {
1528 self.config.custom_shader = if value.is_null() {
1529 None
1530 } else {
1531 Some(value.as_str().ok_or("expected string or null")?.to_string())
1532 };
1533 Ok(())
1534 }
1535 "custom_shader_enabled" => {
1536 self.config.custom_shader_enabled = value.as_bool().ok_or("expected boolean")?;
1537 Ok(())
1538 }
1539 "custom_shader_animation" => {
1540 self.config.custom_shader_animation = value.as_bool().ok_or("expected boolean")?;
1541 Ok(())
1542 }
1543 "custom_shader_animation_speed" => {
1544 self.config.custom_shader_animation_speed = json_as_f32(value)?;
1545 Ok(())
1546 }
1547 "custom_shader_brightness" => {
1548 self.config.custom_shader_brightness = json_as_f32(value)?;
1549 Ok(())
1550 }
1551 "custom_shader_text_opacity" => {
1552 self.config.custom_shader_text_opacity = json_as_f32(value)?;
1553 Ok(())
1554 }
1555 "custom_shader_full_content" => {
1556 self.config.custom_shader_full_content =
1557 value.as_bool().ok_or("expected boolean")?;
1558 Ok(())
1559 }
1560
1561 "cursor_shader" => {
1563 self.config.cursor_shader = if value.is_null() {
1564 None
1565 } else {
1566 Some(value.as_str().ok_or("expected string or null")?.to_string())
1567 };
1568 Ok(())
1569 }
1570 "cursor_shader_enabled" => {
1571 self.config.cursor_shader_enabled = value.as_bool().ok_or("expected boolean")?;
1572 Ok(())
1573 }
1574 "cursor_shader_animation" => {
1575 self.config.cursor_shader_animation = value.as_bool().ok_or("expected boolean")?;
1576 Ok(())
1577 }
1578 "cursor_shader_animation_speed" => {
1579 self.config.cursor_shader_animation_speed = json_as_f32(value)?;
1580 Ok(())
1581 }
1582 "cursor_shader_glow_radius" => {
1583 self.config.cursor_shader_glow_radius = json_as_f32(value)?;
1584 Ok(())
1585 }
1586 "cursor_shader_glow_intensity" => {
1587 self.config.cursor_shader_glow_intensity = json_as_f32(value)?;
1588 Ok(())
1589 }
1590 "cursor_shader_trail_duration" => {
1591 self.config.cursor_shader_trail_duration = json_as_f32(value)?;
1592 Ok(())
1593 }
1594 "cursor_shader_hides_cursor" => {
1595 self.config.cursor_shader_hides_cursor =
1596 value.as_bool().ok_or("expected boolean")?;
1597 Ok(())
1598 }
1599
1600 "window_opacity" => {
1602 self.config.window_opacity = json_as_f32(value)?;
1603 Ok(())
1604 }
1605 "font_size" => {
1606 self.config.font_size = json_as_f32(value)?;
1607 Ok(())
1608 }
1609
1610 _ => Err(format!("unknown or read-only config key: {key}")),
1611 }
1612 }
1613
1614 pub(crate) fn handle_anti_idle(
1618 &mut self,
1619 now: std::time::Instant,
1620 ) -> Option<std::time::Instant> {
1621 if !self.config.anti_idle_enabled {
1622 return None;
1623 }
1624
1625 let idle_threshold = std::time::Duration::from_secs(self.config.anti_idle_seconds.max(1));
1626 let keep_alive_code = [self.config.anti_idle_code];
1627 let mut next_due: Option<std::time::Instant> = None;
1628
1629 for tab in self.tab_manager.tabs_mut() {
1630 if let Ok(term) = tab.terminal.try_lock() {
1631 let current_generation = term.update_generation();
1633 if current_generation > tab.anti_idle_last_generation {
1634 tab.anti_idle_last_generation = current_generation;
1635 tab.anti_idle_last_activity = now;
1636 }
1637
1638 if should_send_keep_alive(tab.anti_idle_last_activity, now, idle_threshold) {
1640 if let Err(e) = term.write(&keep_alive_code) {
1641 log::warn!(
1642 "Failed to send anti-idle keep-alive for tab {}: {}",
1643 tab.id,
1644 e
1645 );
1646 } else {
1647 tab.anti_idle_last_activity = now;
1648 }
1649 }
1650
1651 let elapsed = now.duration_since(tab.anti_idle_last_activity);
1653 let remaining = if elapsed >= idle_threshold {
1654 idle_threshold
1655 } else {
1656 idle_threshold - elapsed
1657 };
1658 let candidate = now + remaining;
1659 next_due = Some(next_due.map_or(candidate, |prev| prev.min(candidate)));
1660 }
1661 }
1662
1663 next_due
1664 }
1665
1666 pub(crate) fn check_shader_reload(&mut self) -> bool {
1671 let Some(watcher) = &self.shader_watcher else {
1672 return false;
1673 };
1674
1675 let Some(event) = watcher.try_recv() else {
1676 return false;
1677 };
1678
1679 self.handle_shader_reload_event(event)
1680 }
1681
1682 fn handle_shader_reload_event(&mut self, event: ShaderReloadEvent) -> bool {
1687 let shader_name = match event.shader_type {
1688 ShaderType::Background => "Background shader",
1689 ShaderType::Cursor => "Cursor shader",
1690 };
1691 let file_name = event
1692 .path
1693 .file_name()
1694 .and_then(|n| n.to_str())
1695 .unwrap_or("shader");
1696
1697 log::info!("Hot reload: {} from {}", shader_name, event.path.display());
1698
1699 let source = match std::fs::read_to_string(&event.path) {
1701 Ok(s) => s,
1702 Err(e) => {
1703 let error_msg = format!("Cannot read '{}': {}", file_name, e);
1704 log::error!("Shader hot reload failed: {}", error_msg);
1705 self.shader_reload_error = Some(error_msg.clone());
1706 match event.shader_type {
1708 ShaderType::Background => {
1709 self.background_shader_reload_result = Some(Some(error_msg.clone()));
1710 }
1711 ShaderType::Cursor => {
1712 self.cursor_shader_reload_result = Some(Some(error_msg.clone()));
1713 }
1714 }
1715 self.deliver_notification(
1717 "Shader Reload Failed",
1718 &format!("{} - {}", shader_name, error_msg),
1719 );
1720 if self.config.notification_bell_visual
1722 && let Some(tab) = self.tab_manager.active_tab_mut()
1723 {
1724 tab.bell.visual_flash = Some(std::time::Instant::now());
1725 }
1726 return false;
1727 }
1728 };
1729
1730 let Some(renderer) = &mut self.renderer else {
1731 log::error!("Cannot reload shader: no renderer available");
1732 return false;
1733 };
1734
1735 let result = match event.shader_type {
1738 ShaderType::Background => renderer.reload_shader_from_source(&source),
1739 ShaderType::Cursor => renderer.reload_cursor_shader_from_source(&source),
1740 };
1741
1742 match result {
1743 Ok(()) => {
1744 log::info!("{} reloaded successfully from {}", shader_name, file_name);
1745 self.shader_reload_error = None;
1746 match event.shader_type {
1748 ShaderType::Background => {
1749 self.background_shader_reload_result = Some(None);
1750 }
1751 ShaderType::Cursor => {
1752 self.cursor_shader_reload_result = Some(None);
1753 }
1754 }
1755 self.needs_redraw = true;
1756 self.request_redraw();
1757 true
1758 }
1759 Err(e) => {
1760 let root_cause = e.root_cause().to_string();
1762 let error_msg = if root_cause.len() > 200 {
1763 format!("{}...", &root_cause[..200])
1765 } else {
1766 root_cause
1767 };
1768
1769 log::error!(
1770 "{} compilation failed (old shader preserved): {}",
1771 shader_name,
1772 error_msg
1773 );
1774 log::debug!("Full error chain: {:#}", e);
1775
1776 self.shader_reload_error = Some(error_msg.clone());
1777 match event.shader_type {
1779 ShaderType::Background => {
1780 self.background_shader_reload_result = Some(Some(error_msg.clone()));
1781 }
1782 ShaderType::Cursor => {
1783 self.cursor_shader_reload_result = Some(Some(error_msg.clone()));
1784 }
1785 }
1786
1787 self.deliver_notification(
1789 "Shader Compilation Error",
1790 &format!("{}: {}", file_name, error_msg),
1791 );
1792
1793 if self.config.notification_bell_visual
1795 && let Some(tab) = self.tab_manager.active_tab_mut()
1796 {
1797 tab.bell.visual_flash = Some(std::time::Instant::now());
1798 }
1799
1800 false
1801 }
1802 }
1803 }
1804
1805 pub(crate) fn is_egui_using_pointer(&self) -> bool {
1807 if self.ai_inspector.wants_pointer() {
1811 return true;
1812 }
1813 if !self.egui_initialized {
1815 return false;
1816 }
1817 if let Some(ctx) = &self.egui_ctx {
1820 ctx.is_using_pointer() || ctx.wants_pointer_input()
1821 } else {
1822 false
1823 }
1824 }
1825
1826 pub(crate) fn has_egui_overlay_visible(&self) -> bool {
1830 self.search_ui.visible
1831 || self.clipboard_history_ui.visible
1832 || self.command_history_ui.visible
1833 || self.shader_install_ui.visible
1834 || self.integrations_ui.visible
1835 || self.ai_inspector.open
1836 }
1837
1838 pub(crate) fn is_egui_using_keyboard(&self) -> bool {
1840 let any_ui_visible = self.help_ui.visible
1844 || self.clipboard_history_ui.visible
1845 || self.command_history_ui.visible
1846 || self.shader_install_ui.visible
1847 || self.integrations_ui.visible
1848 || self.ai_inspector.open;
1849 if !any_ui_visible {
1850 return false;
1851 }
1852
1853 if let Some(ctx) = &self.egui_ctx {
1855 ctx.wants_keyboard_input()
1856 } else {
1857 false
1858 }
1859 }
1860
1861 pub(crate) fn should_show_scrollbar(&self) -> bool {
1863 let tab = match self.tab_manager.active_tab() {
1864 Some(t) => t,
1865 None => return false,
1866 };
1867
1868 if tab.cache.scrollback_len == 0 {
1870 return false;
1871 }
1872
1873 if tab.scroll_state.dragging {
1875 return true;
1876 }
1877
1878 if self.config.scrollbar_autohide_delay == 0 {
1880 return true;
1881 }
1882
1883 if tab.scroll_state.offset > 0 || tab.scroll_state.target_offset > 0 {
1885 return true;
1886 }
1887
1888 if let Some(window) = &self.window {
1890 let padding = 32.0; let width = window.inner_size().width as f64;
1892 let near_right = self.config.scrollbar_position != "left"
1893 && (width - tab.mouse.position.0) <= padding;
1894 let near_left =
1895 self.config.scrollbar_position == "left" && tab.mouse.position.0 <= padding;
1896 if near_left || near_right {
1897 return true;
1898 }
1899 }
1900
1901 tab.scroll_state.last_activity.elapsed().as_millis()
1903 < self.config.scrollbar_autohide_delay as u128
1904 }
1905
1906 pub(crate) fn update_cursor_blink(&mut self) {
1922 if self.config.lock_cursor_style {
1924 if !self.config.cursor_blink {
1925 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1926 return;
1927 }
1928 } else if self.config.lock_cursor_blink && !self.config.cursor_blink {
1929 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1931 return;
1932 }
1933
1934 let cursor_should_blink = if self.config.lock_cursor_style {
1936 self.config.cursor_blink
1938 } else if let Some(tab) = self.tab_manager.active_tab()
1939 && let Ok(term) = tab.terminal.try_lock()
1940 {
1941 use par_term_emu_core_rust::cursor::CursorStyle;
1942 let style = term.cursor_style();
1943 matches!(
1945 style,
1946 CursorStyle::BlinkingBlock
1947 | CursorStyle::BlinkingUnderline
1948 | CursorStyle::BlinkingBar
1949 )
1950 } else {
1951 self.config.cursor_blink
1953 };
1954
1955 if !cursor_should_blink {
1956 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1958 return;
1959 }
1960
1961 let now = std::time::Instant::now();
1962
1963 if let Some(last_key) = self.last_key_press
1965 && now.duration_since(last_key).as_millis() < 500
1966 {
1967 self.cursor_opacity = (self.cursor_opacity + 0.1).min(1.0);
1968 self.last_cursor_blink = Some(now);
1969 return;
1970 }
1971
1972 let blink_interval = std::time::Duration::from_millis(self.config.cursor_blink_interval);
1974
1975 if let Some(last_blink) = self.last_cursor_blink {
1976 let elapsed = now.duration_since(last_blink);
1977 let progress = (elapsed.as_millis() as f32) / (blink_interval.as_millis() as f32);
1978
1979 self.cursor_opacity = ((progress * std::f32::consts::PI).cos())
1981 .abs()
1982 .clamp(0.0, 1.0);
1983
1984 if elapsed >= blink_interval * 2 {
1986 self.last_cursor_blink = Some(now);
1987 }
1988 } else {
1989 self.cursor_opacity = 1.0;
1991 self.last_cursor_blink = Some(now);
1992 }
1993 }
1994
1995 pub(crate) fn render(&mut self) {
1997 if self.is_shutting_down {
1999 return;
2000 }
2001
2002 let target_fps = if self.config.pause_refresh_on_blur && !self.is_focused {
2006 self.config.unfocused_fps
2007 } else {
2008 self.config.max_fps
2009 };
2010 let frame_interval_ms = 1000 / target_fps.max(1);
2011 let frame_interval = std::time::Duration::from_millis(frame_interval_ms as u64);
2012
2013 if let Some(last_render) = self.last_render_time {
2014 let elapsed = last_render.elapsed();
2015 if elapsed < frame_interval {
2016 return;
2018 }
2019 }
2020
2021 self.last_render_time = Some(std::time::Instant::now());
2023
2024 let absolute_start = std::time::Instant::now();
2025
2026 self.needs_redraw = false;
2029
2030 let frame_start = std::time::Instant::now();
2032
2033 if let Some(last_start) = self.debug.last_frame_start {
2035 let frame_time = frame_start.duration_since(last_start);
2036 self.debug.frame_times.push(frame_time);
2037 if self.debug.frame_times.len() > 60 {
2038 self.debug.frame_times.remove(0);
2039 }
2040 }
2041 self.debug.last_frame_start = Some(frame_start);
2042
2043 let animation_running = if let Some(tab) = self.tab_manager.active_tab_mut() {
2045 tab.scroll_state.update_animation()
2046 } else {
2047 false
2048 };
2049
2050 self.tab_manager.update_all_titles();
2052
2053 if self.pending_font_rebuild {
2055 if let Err(e) = self.rebuild_renderer() {
2056 log::error!("Failed to rebuild renderer after font change: {}", e);
2057 }
2058 self.pending_font_rebuild = false;
2059 }
2060
2061 let tab_count = self.tab_manager.tab_count();
2064 let tab_bar_height = self.tab_bar_ui.get_height(tab_count, &self.config);
2065 let tab_bar_width = self.tab_bar_ui.get_width(tab_count, &self.config);
2066 crate::debug_trace!(
2067 "TAB_SYNC",
2068 "Tab count={}, tab_bar_height={:.0}, tab_bar_width={:.0}, position={:?}, mode={:?}",
2069 tab_count,
2070 tab_bar_height,
2071 tab_bar_width,
2072 self.config.tab_bar_position,
2073 self.config.tab_bar_mode
2074 );
2075 if let Some(renderer) = &mut self.renderer {
2076 let grid_changed = Self::apply_tab_bar_offsets_for_position(
2077 self.config.tab_bar_position,
2078 renderer,
2079 tab_bar_height,
2080 tab_bar_width,
2081 );
2082 if let Some((new_cols, new_rows)) = grid_changed {
2083 let cell_width = renderer.cell_width();
2084 let cell_height = renderer.cell_height();
2085 let width_px = (new_cols as f32 * cell_width) as usize;
2086 let height_px = (new_rows as f32 * cell_height) as usize;
2087
2088 for tab in self.tab_manager.tabs_mut() {
2089 if let Ok(mut term) = tab.terminal.try_lock() {
2090 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
2091 let _ = term.resize_with_pixels(new_cols, new_rows, width_px, height_px);
2092 }
2093 tab.cache.cells = None;
2094 }
2095 crate::debug_info!(
2096 "TAB_SYNC",
2097 "Tab bar offsets changed (position={:?}), resized terminals to {}x{}",
2098 self.config.tab_bar_position,
2099 new_cols,
2100 new_rows
2101 );
2102 }
2103 }
2104
2105 self.sync_status_bar_inset();
2108
2109 let (renderer_size, visible_lines, grid_cols) = if let Some(renderer) = &self.renderer {
2110 let (cols, rows) = renderer.grid_size();
2111 (renderer.size(), rows, cols)
2112 } else {
2113 return;
2114 };
2115
2116 let (
2118 terminal,
2119 scroll_offset,
2120 mouse_selection,
2121 cache_cells,
2122 cache_generation,
2123 cache_scroll_offset,
2124 cache_cursor_pos,
2125 cache_selection,
2126 cached_scrollback_len,
2127 cached_terminal_title,
2128 hovered_url,
2129 ) = match self.tab_manager.active_tab() {
2130 Some(t) => (
2131 t.terminal.clone(),
2132 t.scroll_state.offset,
2133 t.mouse.selection,
2134 t.cache.cells.clone(),
2135 t.cache.generation,
2136 t.cache.scroll_offset,
2137 t.cache.cursor_pos,
2138 t.cache.selection,
2139 t.cache.scrollback_len,
2140 t.cache.terminal_title.clone(),
2141 t.mouse.hovered_url.clone(),
2142 ),
2143 None => return,
2144 };
2145
2146 let _is_running = if let Ok(term) = terminal.try_lock() {
2148 term.is_running()
2149 } else {
2150 true };
2152
2153 if animation_running && let Some(window) = &self.window {
2155 window.request_redraw();
2156 }
2157
2158 let (cells, current_cursor_pos, cursor_style, is_alt_screen) = if let Ok(term) =
2163 terminal.try_lock()
2164 {
2165 let current_generation = term.update_generation();
2167
2168 let (selection, rectangular) = if let Some(sel) = mouse_selection {
2170 (
2171 Some(sel.normalized()),
2172 sel.mode == SelectionMode::Rectangular,
2173 )
2174 } else {
2175 (None, false)
2176 };
2177
2178 let cursor_visible = self.config.lock_cursor_visibility || term.is_cursor_visible();
2183 let current_cursor_pos = if self.copy_mode.active {
2184 self.copy_mode.screen_cursor_pos(scroll_offset)
2185 } else if scroll_offset == 0 && cursor_visible {
2186 Some(term.cursor_position())
2187 } else {
2188 None
2189 };
2190
2191 let cursor = current_cursor_pos.map(|pos| (pos, self.cursor_opacity));
2192
2193 let cursor_style = if self.copy_mode.active && current_cursor_pos.is_some() {
2198 Some(TermCursorStyle::SteadyBlock)
2199 } else if current_cursor_pos.is_some() {
2200 if self.config.lock_cursor_style {
2201 let style = if self.config.cursor_blink {
2203 match self.config.cursor_style {
2204 CursorStyle::Block => TermCursorStyle::BlinkingBlock,
2205 CursorStyle::Beam => TermCursorStyle::BlinkingBar,
2206 CursorStyle::Underline => TermCursorStyle::BlinkingUnderline,
2207 }
2208 } else {
2209 match self.config.cursor_style {
2210 CursorStyle::Block => TermCursorStyle::SteadyBlock,
2211 CursorStyle::Beam => TermCursorStyle::SteadyBar,
2212 CursorStyle::Underline => TermCursorStyle::SteadyUnderline,
2213 }
2214 };
2215 Some(style)
2216 } else {
2217 let mut style = term.cursor_style();
2218 if self.config.lock_cursor_blink && !self.config.cursor_blink {
2220 style = match style {
2221 TermCursorStyle::BlinkingBlock => TermCursorStyle::SteadyBlock,
2222 TermCursorStyle::BlinkingBar => TermCursorStyle::SteadyBar,
2223 TermCursorStyle::BlinkingUnderline => TermCursorStyle::SteadyUnderline,
2224 other => other,
2225 };
2226 }
2227 Some(style)
2228 }
2229 } else {
2230 None
2231 };
2232
2233 log::trace!(
2234 "Cursor: pos={:?}, opacity={:.2}, style={:?}, scroll={}, visible={}",
2235 current_cursor_pos,
2236 self.cursor_opacity,
2237 cursor_style,
2238 scroll_offset,
2239 term.is_cursor_visible()
2240 );
2241
2242 let needs_regeneration = cache_cells.is_none()
2245 || current_generation != cache_generation
2246 || scroll_offset != cache_scroll_offset
2247 || current_cursor_pos != cache_cursor_pos || mouse_selection != cache_selection; let cell_gen_start = std::time::Instant::now();
2251 let (cells, is_cache_hit) = if needs_regeneration {
2252 let fresh_cells =
2254 term.get_cells_with_scrollback(scroll_offset, selection, rectangular, cursor);
2255
2256 (fresh_cells, false)
2257 } else {
2258 (cache_cells.as_ref().unwrap().clone(), true)
2261 };
2262 self.debug.cache_hit = is_cache_hit;
2263 self.debug.cell_gen_time = cell_gen_start.elapsed();
2264
2265 let is_alt_screen = term.is_alt_screen_active();
2267
2268 (cells, current_cursor_pos, cursor_style, is_alt_screen)
2269 } else {
2270 return; };
2272
2273 let resolved_hides_cursor = self
2277 .config
2278 .cursor_shader
2279 .as_ref()
2280 .and_then(|name| self.config.cursor_shader_configs.get(name))
2281 .and_then(|override_cfg| override_cfg.hides_cursor)
2282 .or_else(|| {
2283 self.config
2284 .cursor_shader
2285 .as_ref()
2286 .and_then(|name| self.cursor_shader_metadata_cache.get(name))
2287 .and_then(|meta| meta.defaults.hides_cursor)
2288 })
2289 .unwrap_or(self.config.cursor_shader_hides_cursor);
2290 let resolved_disable_in_alt_screen = self
2292 .config
2293 .cursor_shader
2294 .as_ref()
2295 .and_then(|name| self.config.cursor_shader_configs.get(name))
2296 .and_then(|override_cfg| override_cfg.disable_in_alt_screen)
2297 .or_else(|| {
2298 self.config
2299 .cursor_shader
2300 .as_ref()
2301 .and_then(|name| self.cursor_shader_metadata_cache.get(name))
2302 .and_then(|meta| meta.defaults.disable_in_alt_screen)
2303 })
2304 .unwrap_or(self.config.cursor_shader_disable_in_alt_screen);
2305 let hide_cursor_for_shader = self.config.cursor_shader_enabled
2306 && resolved_hides_cursor
2307 && !(resolved_disable_in_alt_screen && is_alt_screen);
2308 if let Some(renderer) = &mut self.renderer {
2309 renderer.set_cursor_hidden_for_shader(hide_cursor_for_shader);
2310 }
2311
2312 if !self.debug.cache_hit
2315 && let Some(tab) = self.tab_manager.active_tab_mut()
2316 && let Ok(term) = tab.terminal.try_lock()
2317 {
2318 let current_generation = term.update_generation();
2319 tab.cache.cells = Some(cells.clone());
2320 tab.cache.generation = current_generation;
2321 tab.cache.scroll_offset = tab.scroll_state.offset;
2322 tab.cache.cursor_pos = current_cursor_pos;
2323 tab.cache.selection = tab.mouse.selection;
2324 }
2325
2326 let mut show_scrollbar = self.should_show_scrollbar();
2327
2328 let (scrollback_len, terminal_title) = if let Ok(mut term) = terminal.try_lock() {
2329 let cursor_row = current_cursor_pos.map(|(_, row)| row).unwrap_or(0);
2331 let sb_len = term.scrollback_len();
2332 term.update_scrollback_metadata(sb_len, cursor_row);
2333
2334 for mark in term.scrollback_marks() {
2341 if let Some(ref cmd) = mark.command
2342 && !cmd.is_empty()
2343 && self.synced_commands.insert(cmd.clone())
2344 {
2345 self.command_history
2346 .add(cmd.clone(), mark.exit_code, mark.duration_ms);
2347 }
2348 }
2349 for (cmd, exit_code, duration_ms) in term.core_command_history() {
2350 if !cmd.is_empty() && self.synced_commands.insert(cmd.clone()) {
2351 self.command_history.add(cmd, exit_code, duration_ms);
2352 }
2353 }
2354
2355 (sb_len, term.get_title())
2356 } else {
2357 (cached_scrollback_len, cached_terminal_title.clone())
2358 };
2359
2360 if let Some(tab) = self.tab_manager.active_tab_mut() {
2362 tab.cache.scrollback_len = scrollback_len;
2363 tab.scroll_state
2364 .clamp_to_scrollback(tab.cache.scrollback_len);
2365 }
2366
2367 if self.copy_mode.active
2369 && let Ok(term) = terminal.try_lock()
2370 {
2371 let (cols, rows) = term.dimensions();
2372 self.copy_mode.update_dimensions(cols, rows, scrollback_len);
2373 }
2374
2375 let need_marks =
2376 self.config.scrollbar_command_marks || self.config.command_separator_enabled;
2377 let mut scrollback_marks = if need_marks {
2378 if let Ok(term) = terminal.try_lock() {
2379 term.scrollback_marks()
2380 } else {
2381 Vec::new()
2382 }
2383 } else {
2384 Vec::new()
2385 };
2386
2387 if let Some(tab) = self.tab_manager.active_tab() {
2389 scrollback_marks.extend(tab.trigger_marks.iter().cloned());
2390 }
2391
2392 if !scrollback_marks.is_empty() {
2394 show_scrollbar = true;
2395 }
2396
2397 if self.config.allow_title_change
2400 && hovered_url.is_none()
2401 && terminal_title != cached_terminal_title
2402 {
2403 if let Some(tab) = self.tab_manager.active_tab_mut() {
2404 tab.cache.terminal_title = terminal_title.clone();
2405 }
2406 if let Some(window) = &self.window {
2407 if terminal_title.is_empty() {
2408 window.set_title(&self.format_title(&self.config.window_title));
2410 } else {
2411 window.set_title(&self.format_title(&terminal_title));
2413 }
2414 }
2415 }
2416
2417 let total_lines = visible_lines + scrollback_len;
2419
2420 let url_detect_start = std::time::Instant::now();
2423 let debug_url_detect_time = if !self.debug.cache_hit {
2424 self.detect_urls();
2426 url_detect_start.elapsed()
2427 } else {
2428 std::time::Duration::ZERO
2430 };
2431
2432 let url_underline_start = std::time::Instant::now();
2434 let mut cells = cells; self.apply_url_underlines(&mut cells, &renderer_size);
2436 let _debug_url_underline_time = url_underline_start.elapsed();
2437
2438 if self.search_ui.visible {
2440 if let Some(tab) = self.tab_manager.active_tab()
2442 && let Ok(term) = tab.terminal.try_lock()
2443 {
2444 let lines_iter =
2445 crate::app::search_highlight::get_all_searchable_lines(&term, visible_lines);
2446 self.search_ui.update_search(lines_iter);
2447 }
2448
2449 let scroll_offset = self
2451 .tab_manager
2452 .active_tab()
2453 .map(|t| t.scroll_state.offset)
2454 .unwrap_or(0);
2455 self.apply_search_highlights(
2457 &mut cells,
2458 grid_cols,
2459 scroll_offset,
2460 scrollback_len,
2461 visible_lines,
2462 );
2463 }
2464
2465 self.update_cursor_blink();
2467
2468 let render_start = std::time::Instant::now();
2469
2470 let mut debug_update_cells_time = std::time::Duration::ZERO;
2471 #[allow(unused_assignments)]
2472 let mut debug_graphics_time = std::time::Duration::ZERO;
2473 #[allow(unused_assignments)]
2474 let mut debug_actual_render_time = std::time::Duration::ZERO;
2475 let _ = &debug_actual_render_time;
2476 let mut pending_clipboard_action = ClipboardHistoryAction::None;
2478 let mut pending_command_history_action = CommandHistoryAction::None;
2480 let mut pending_paste_special_action = PasteSpecialAction::None;
2482 let mut pending_session_picker_action = SessionPickerAction::None;
2484 let mut pending_tab_action = TabBarAction::None;
2486 let mut pending_shader_install_response = ShaderInstallResponse::None;
2488 let mut pending_integrations_response = IntegrationsResponse::default();
2490 let mut pending_search_action = crate::search::SearchAction::None;
2492 let mut pending_inspector_action = InspectorAction::None;
2494 let mut pending_profile_drawer_action = ProfileDrawerAction::None;
2496 let mut pending_close_confirm_action = CloseConfirmAction::None;
2498 let mut pending_quit_confirm_action = QuitConfirmAction::None;
2500 let mut pending_remote_install_action = RemoteShellInstallAction::None;
2501 let mut pending_ssh_connect_action = SshConnectAction::None;
2502
2503 let msg_count_before = self.ai_inspector.chat.messages.len();
2505 type ConfigUpdateEntry = (
2508 std::collections::HashMap<String, serde_json::Value>,
2509 tokio::sync::oneshot::Sender<Result<(), String>>,
2510 );
2511 let mut pending_config_updates: Vec<ConfigUpdateEntry> = Vec::new();
2512 if let Some(rx) = &mut self.agent_rx {
2513 while let Ok(msg) = rx.try_recv() {
2514 match msg {
2515 AgentMessage::StatusChanged(status) => {
2516 self.ai_inspector.chat.flush_agent_message();
2518 self.ai_inspector.agent_status = status;
2519 self.needs_redraw = true;
2520 }
2521 AgentMessage::SessionUpdate(update) => {
2522 self.ai_inspector.chat.handle_update(update);
2523 self.needs_redraw = true;
2524 }
2525 AgentMessage::PermissionRequest {
2526 request_id,
2527 tool_call,
2528 options,
2529 } => {
2530 log::info!(
2531 "ACP: permission request id={request_id} options={}",
2532 options.len()
2533 );
2534 let description = tool_call
2535 .get("title")
2536 .and_then(|t| t.as_str())
2537 .unwrap_or("Permission requested")
2538 .to_string();
2539 self.ai_inspector
2540 .chat
2541 .messages
2542 .push(ChatMessage::Permission {
2543 request_id,
2544 description,
2545 options: options
2546 .iter()
2547 .map(|o| (o.option_id.clone(), o.name.clone()))
2548 .collect(),
2549 resolved: false,
2550 });
2551 self.needs_redraw = true;
2552 }
2553 AgentMessage::PromptComplete => {
2554 self.ai_inspector.chat.flush_agent_message();
2555 self.needs_redraw = true;
2556 }
2557 AgentMessage::ConfigUpdate { updates, reply } => {
2558 pending_config_updates.push((updates, reply));
2559 }
2560 AgentMessage::ClientReady(client) => {
2561 log::info!("ACP: agent_client ready");
2562 self.agent_client = Some(client);
2563 }
2564 }
2565 }
2566 }
2567 for (updates, reply) in pending_config_updates {
2569 let result = self.apply_agent_config_updates(&updates);
2570 if result.is_ok() {
2571 self.config_changed_by_agent = true;
2572 }
2573 let _ = reply.send(result);
2574 self.needs_redraw = true;
2575 }
2576
2577 if self.config.ai_inspector_agent_terminal_access {
2579 let new_messages = &self.ai_inspector.chat.messages[msg_count_before..];
2580 let commands_to_run: Vec<String> = new_messages
2581 .iter()
2582 .filter_map(|msg| {
2583 if let ChatMessage::CommandSuggestion(cmd) = msg {
2584 Some(format!("{cmd}\n"))
2585 } else {
2586 None
2587 }
2588 })
2589 .collect();
2590
2591 if !commands_to_run.is_empty()
2592 && let Some(tab) = self.tab_manager.active_tab()
2593 && let Ok(term) = tab.terminal.try_lock()
2594 {
2595 for cmd in &commands_to_run {
2596 let _ = term.write(cmd.as_bytes());
2597 }
2598 crate::debug_info!(
2599 "AI_INSPECTOR",
2600 "Auto-executed {} command(s) in terminal",
2601 commands_to_run.len()
2602 );
2603 }
2604 }
2605
2606 if self.ai_inspector.open
2610 && let Some(tab) = self.tab_manager.active_tab()
2611 && let Ok(term) = tab.terminal.try_lock()
2612 {
2613 let history = term.core_command_history();
2614 let current_count = history.len();
2615
2616 if current_count != self.ai_inspector.last_command_count {
2617 let had_commands = self.ai_inspector.last_command_count > 0;
2619 self.ai_inspector.last_command_count = current_count;
2620 self.ai_inspector.needs_refresh = true;
2621
2622 if had_commands
2624 && current_count > 0
2625 && self.config.ai_inspector_auto_context
2626 && self.ai_inspector.agent_status == AgentStatus::Connected
2627 && let Some((cmd, exit_code, duration_ms)) = history.last()
2628 {
2629 let exit_code_str = exit_code
2630 .map(|c| c.to_string())
2631 .unwrap_or_else(|| "N/A".to_string());
2632 let duration = duration_ms.unwrap_or(0);
2633
2634 let cwd = term.shell_integration_cwd().unwrap_or_default();
2635
2636 let context = format!(
2637 "Command completed:\n$ {}\nExit code: {}\nDuration: {}ms\nCWD: {}",
2638 cmd, exit_code_str, duration, cwd
2639 );
2640
2641 if let Some(agent) = &self.agent {
2642 let agent = agent.clone();
2643 let content = vec![par_term_acp::ContentBlock::Text { text: context }];
2644 self.runtime.spawn(async move {
2645 let agent = agent.lock().await;
2646 let _ = agent.send_prompt(content).await;
2647 });
2648 }
2649 }
2650 }
2651 }
2652
2653 if self.ai_inspector.open
2655 && self.ai_inspector.needs_refresh
2656 && let Some(tab) = self.tab_manager.active_tab()
2657 && let Ok(term) = tab.terminal.try_lock()
2658 {
2659 let snapshot = crate::ai_inspector::snapshot::SnapshotData::gather(
2660 &term,
2661 &self.ai_inspector.scope,
2662 self.config.ai_inspector_context_max_lines,
2663 );
2664 self.ai_inspector.snapshot = Some(snapshot);
2665 self.ai_inspector.needs_refresh = false;
2666 }
2667
2668 let is_tmux_gateway = self.is_gateway_active();
2673 let effective_pane_padding = if is_tmux_gateway {
2674 0.0
2675 } else {
2676 self.config.pane_padding
2677 };
2678
2679 let is_tmux_connected = self.is_tmux_connected();
2682 let status_bar_height =
2683 crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux_connected);
2684
2685 let custom_status_bar_height = self.status_bar_ui.height(&self.config, self.is_fullscreen);
2687
2688 let window_size_for_badge = self.renderer.as_ref().map(|r| r.size());
2690
2691 let progress_snapshot = if self.config.progress_bar_enabled {
2693 self.tab_manager.active_tab().and_then(|tab| {
2694 tab.terminal
2695 .try_lock()
2696 .ok()
2697 .map(|term| ProgressBarSnapshot {
2698 simple: term.progress_bar(),
2699 named: term.named_progress_bars(),
2700 })
2701 })
2702 } else {
2703 None
2704 };
2705
2706 self.sync_ai_inspector_width();
2709
2710 if let Some(renderer) = &mut self.renderer {
2711 let disable_cursor_shader =
2716 self.config.cursor_shader_disable_in_alt_screen && is_alt_screen;
2717 renderer.set_cursor_shader_disabled_for_alt_screen(disable_cursor_shader);
2718
2719 if !self.debug.cache_hit {
2722 let t = std::time::Instant::now();
2723 renderer.update_cells(&cells);
2724 debug_update_cells_time = t.elapsed();
2725 }
2726
2727 if let (Some(pos), Some(opacity), Some(style)) =
2729 (current_cursor_pos, Some(self.cursor_opacity), cursor_style)
2730 {
2731 renderer.update_cursor(pos, opacity, style);
2732 let cursor_color = [
2735 self.config.cursor_color[0] as f32 / 255.0,
2736 self.config.cursor_color[1] as f32 / 255.0,
2737 self.config.cursor_color[2] as f32 / 255.0,
2738 1.0,
2739 ];
2740 renderer.update_shader_cursor(pos.0, pos.1, opacity, cursor_color, style);
2741 } else {
2742 renderer.clear_cursor();
2743 }
2744
2745 if let Some(ref snap) = progress_snapshot {
2747 use par_term_emu_core_rust::terminal::ProgressState;
2748 let state_val = match snap.simple.state {
2749 ProgressState::Hidden => 0.0,
2750 ProgressState::Normal => 1.0,
2751 ProgressState::Error => 2.0,
2752 ProgressState::Indeterminate => 3.0,
2753 ProgressState::Warning => 4.0,
2754 };
2755 let active_count = (if snap.simple.is_active() { 1 } else { 0 })
2756 + snap.named.values().filter(|b| b.state.is_active()).count();
2757 renderer.update_shader_progress(
2758 state_val,
2759 snap.simple.progress as f32 / 100.0,
2760 if snap.has_active() { 1.0 } else { 0.0 },
2761 active_count as f32,
2762 );
2763 } else {
2764 renderer.update_shader_progress(0.0, 0.0, 0.0, 0.0);
2765 }
2766
2767 let scroll_offset = self
2769 .tab_manager
2770 .active_tab()
2771 .map(|t| t.scroll_state.offset)
2772 .unwrap_or(0);
2773 renderer.update_scrollbar(scroll_offset, visible_lines, total_lines, &scrollback_marks);
2774
2775 if self.config.command_separator_enabled {
2777 let separator_marks = crate::renderer::compute_visible_separator_marks(
2778 &scrollback_marks,
2779 scrollback_len,
2780 scroll_offset,
2781 visible_lines,
2782 );
2783 renderer.set_separator_marks(separator_marks);
2784 } else {
2785 renderer.set_separator_marks(Vec::new());
2786 }
2787
2788 let anim_start = std::time::Instant::now();
2790 if let Some(tab) = self.tab_manager.active_tab() {
2791 let terminal = tab.terminal.blocking_lock();
2792 if terminal.update_animations() {
2793 if let Some(window) = &self.window {
2795 window.request_redraw();
2796 }
2797 }
2798 }
2799 let debug_anim_time = anim_start.elapsed();
2800
2801 let graphics_start = std::time::Instant::now();
2805 if let Some(tab) = self.tab_manager.active_tab() {
2806 let terminal = tab.terminal.blocking_lock();
2807 let mut graphics = terminal.get_graphics_with_animations();
2808 let scrollback_len = terminal.scrollback_len();
2809
2810 let scrollback_graphics = terminal.get_scrollback_graphics();
2812 let scrollback_count = scrollback_graphics.len();
2813 graphics.extend(scrollback_graphics);
2814
2815 debug_info!(
2816 "APP",
2817 "Got {} graphics ({} scrollback) from terminal (scroll_offset={}, scrollback_len={})",
2818 graphics.len(),
2819 scrollback_count,
2820 scroll_offset,
2821 scrollback_len
2822 );
2823 if let Err(e) = renderer.update_graphics(
2824 &graphics,
2825 scroll_offset,
2826 scrollback_len,
2827 visible_lines,
2828 ) {
2829 log::error!("Failed to update graphics: {}", e);
2830 }
2831 }
2832 debug_graphics_time = graphics_start.elapsed();
2833
2834 let visual_bell_flash = self
2836 .tab_manager
2837 .active_tab()
2838 .and_then(|t| t.bell.visual_flash);
2839 let visual_bell_intensity = if let Some(flash_start) = visual_bell_flash {
2840 const FLASH_DURATION_MS: u128 = 150;
2841 let elapsed = flash_start.elapsed().as_millis();
2842 if elapsed < FLASH_DURATION_MS {
2843 if let Some(window) = &self.window {
2845 window.request_redraw();
2846 }
2847 0.3 * (1.0 - (elapsed as f32 / FLASH_DURATION_MS as f32))
2849 } else {
2850 if let Some(tab) = self.tab_manager.active_tab_mut() {
2852 tab.bell.visual_flash = None;
2853 }
2854 0.0
2855 }
2856 } else {
2857 0.0
2858 };
2859
2860 renderer.set_visual_bell_intensity(visual_bell_intensity);
2862
2863 let egui_start = std::time::Instant::now();
2865
2866 let show_fps = self.debug.show_fps_overlay;
2868 let fps_value = self.debug.fps_value;
2869 let frame_time_ms = if !self.debug.frame_times.is_empty() {
2870 let avg = self.debug.frame_times.iter().sum::<std::time::Duration>()
2871 / self.debug.frame_times.len() as u32;
2872 avg.as_secs_f64() * 1000.0
2873 } else {
2874 0.0
2875 };
2876
2877 let badge_enabled = self.badge_state.enabled;
2879 let badge_state = if badge_enabled {
2880 if self.badge_state.is_dirty() {
2882 self.badge_state.interpolate();
2883 }
2884 Some(self.badge_state.clone())
2885 } else {
2886 None
2887 };
2888
2889 let status_bar_session_vars = if self.config.status_bar_enabled {
2891 Some(self.badge_state.variables.read().clone())
2892 } else {
2893 None
2894 };
2895
2896 let hovered_mark: Option<crate::scrollback_metadata::ScrollbackMark> =
2898 if self.config.scrollbar_mark_tooltips && self.config.scrollbar_command_marks {
2899 self.tab_manager
2900 .active_tab()
2901 .map(|tab| tab.mouse.position)
2902 .and_then(|(mx, my)| {
2903 renderer.scrollbar_mark_at_position(mx as f32, my as f32, 8.0)
2904 })
2905 .cloned()
2906 } else {
2907 None
2908 };
2909
2910 let pane_identify_bounds: Vec<(usize, crate::pane::PaneBounds)> =
2912 if self.pane_identify_hide_time.is_some() {
2913 self.tab_manager
2914 .active_tab()
2915 .and_then(|tab| tab.pane_manager())
2916 .map(|pm| {
2917 pm.all_panes()
2918 .iter()
2919 .enumerate()
2920 .map(|(i, pane)| (i, pane.bounds))
2921 .collect()
2922 })
2923 .unwrap_or_default()
2924 } else {
2925 Vec::new()
2926 };
2927
2928 let egui_data = if let (Some(egui_ctx), Some(egui_state)) =
2929 (&self.egui_ctx, &mut self.egui_state)
2930 {
2931 let mut raw_input = egui_state.take_egui_input(self.window.as_ref().unwrap());
2932
2933 raw_input.events.append(&mut self.pending_egui_events);
2936
2937 let any_modal_visible = self.help_ui.visible
2943 || self.clipboard_history_ui.visible
2944 || self.command_history_ui.visible
2945 || self.shader_install_ui.visible
2946 || self.integrations_ui.visible
2947 || self.search_ui.visible
2948 || self.tmux_session_picker_ui.visible
2949 || self.ssh_connect_ui.is_visible()
2950 || self.quit_confirmation_ui.is_visible();
2951 if !any_modal_visible {
2952 raw_input.events.retain(|e| {
2953 !matches!(
2954 e,
2955 egui::Event::Key {
2956 key: egui::Key::Tab,
2957 ..
2958 }
2959 )
2960 });
2961 }
2962
2963 let egui_output = egui_ctx.run(raw_input, |ctx| {
2964 if show_fps {
2966 egui::Area::new(egui::Id::new("fps_overlay"))
2967 .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-30.0, 10.0))
2968 .order(egui::Order::Foreground)
2969 .show(ctx, |ui| {
2970 egui::Frame::NONE
2971 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
2972 .inner_margin(egui::Margin::same(8))
2973 .corner_radius(4.0)
2974 .show(ui, |ui| {
2975 ui.style_mut().visuals.override_text_color =
2976 Some(egui::Color32::from_rgb(0, 255, 0));
2977 ui.label(
2978 egui::RichText::new(format!(
2979 "FPS: {:.1}\nFrame: {:.2}ms",
2980 fps_value, frame_time_ms
2981 ))
2982 .monospace()
2983 .size(14.0),
2984 );
2985 });
2986 });
2987 }
2988
2989 if self.resize_overlay_visible
2991 && let Some((width_px, height_px, cols, rows)) = self.resize_dimensions
2992 {
2993 egui::Area::new(egui::Id::new("resize_overlay"))
2994 .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
2995 .order(egui::Order::Foreground)
2996 .show(ctx, |ui| {
2997 egui::Frame::NONE
2998 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 220))
2999 .inner_margin(egui::Margin::same(16))
3000 .corner_radius(8.0)
3001 .show(ui, |ui| {
3002 ui.style_mut().visuals.override_text_color =
3003 Some(egui::Color32::from_rgb(255, 255, 255));
3004 ui.label(
3005 egui::RichText::new(format!(
3006 "{}×{}\n{}×{} px",
3007 cols, rows, width_px, height_px
3008 ))
3009 .monospace()
3010 .size(24.0),
3011 );
3012 });
3013 });
3014 }
3015
3016 if self.copy_mode.active && self.config.copy_mode_show_status {
3018 let status = self.copy_mode.status_text();
3019 let (mode_text, color) = if self.copy_mode.is_searching {
3020 ("SEARCH", egui::Color32::from_rgb(255, 165, 0))
3021 } else {
3022 match self.copy_mode.visual_mode {
3023 crate::copy_mode::VisualMode::None => {
3024 ("COPY", egui::Color32::from_rgb(100, 200, 100))
3025 }
3026 crate::copy_mode::VisualMode::Char => {
3027 ("VISUAL", egui::Color32::from_rgb(100, 150, 255))
3028 }
3029 crate::copy_mode::VisualMode::Line => {
3030 ("V-LINE", egui::Color32::from_rgb(100, 150, 255))
3031 }
3032 crate::copy_mode::VisualMode::Block => {
3033 ("V-BLOCK", egui::Color32::from_rgb(100, 150, 255))
3034 }
3035 }
3036 };
3037
3038 egui::Area::new(egui::Id::new("copy_mode_status_bar"))
3039 .anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(0.0, 0.0))
3040 .order(egui::Order::Foreground)
3041 .show(ctx, |ui| {
3042 let available_width = ui.available_width();
3043 egui::Frame::NONE
3044 .fill(egui::Color32::from_rgba_unmultiplied(40, 40, 40, 230))
3045 .inner_margin(egui::Margin::symmetric(12, 6))
3046 .show(ui, |ui| {
3047 ui.set_min_width(available_width);
3048 ui.horizontal(|ui| {
3049 ui.label(
3050 egui::RichText::new(mode_text)
3051 .monospace()
3052 .size(13.0)
3053 .color(color)
3054 .strong(),
3055 );
3056 ui.separator();
3057 ui.label(
3058 egui::RichText::new(&status)
3059 .monospace()
3060 .size(12.0)
3061 .color(egui::Color32::from_rgb(200, 200, 200)),
3062 );
3063 });
3064 });
3065 });
3066 }
3067
3068 if let Some(ref message) = self.toast_message {
3070 egui::Area::new(egui::Id::new("toast_notification"))
3071 .anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, 60.0))
3072 .order(egui::Order::Foreground)
3073 .show(ctx, |ui| {
3074 egui::Frame::NONE
3075 .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
3076 .inner_margin(egui::Margin::symmetric(20, 12))
3077 .corner_radius(8.0)
3078 .stroke(egui::Stroke::new(
3079 1.0,
3080 egui::Color32::from_rgb(80, 80, 80),
3081 ))
3082 .show(ui, |ui| {
3083 ui.style_mut().visuals.override_text_color =
3084 Some(egui::Color32::from_rgb(255, 255, 255));
3085 ui.label(egui::RichText::new(message).size(16.0));
3086 });
3087 });
3088 }
3089
3090 if let Some(ref mark) = hovered_mark {
3092 let mut lines = Vec::new();
3094
3095 if let Some(ref cmd) = mark.command {
3096 let truncated = if cmd.len() > 50 {
3097 format!("{}...", &cmd[..47])
3098 } else {
3099 cmd.clone()
3100 };
3101 lines.push(format!("Command: {}", truncated));
3102 }
3103
3104 if let Some(start_time) = mark.start_time {
3105 use chrono::{DateTime, Local, Utc};
3106 let dt =
3107 DateTime::<Utc>::from_timestamp_millis(start_time as i64).unwrap();
3108 let local: DateTime<Local> = dt.into();
3109 lines.push(format!("Time: {}", local.format("%H:%M:%S")));
3110 }
3111
3112 if let Some(duration_ms) = mark.duration_ms {
3113 if duration_ms < 1000 {
3114 lines.push(format!("Duration: {}ms", duration_ms));
3115 } else if duration_ms < 60000 {
3116 lines
3117 .push(format!("Duration: {:.1}s", duration_ms as f64 / 1000.0));
3118 } else {
3119 let mins = duration_ms / 60000;
3120 let secs = (duration_ms % 60000) / 1000;
3121 lines.push(format!("Duration: {}m {}s", mins, secs));
3122 }
3123 }
3124
3125 if let Some(exit_code) = mark.exit_code {
3126 lines.push(format!("Exit: {}", exit_code));
3127 }
3128
3129 let tooltip_text = lines.join("\n");
3130
3131 let mouse_pos = ctx.pointer_hover_pos().unwrap_or(egui::pos2(100.0, 100.0));
3133 let tooltip_x = (mouse_pos.x - 180.0).max(10.0);
3134 let tooltip_y = (mouse_pos.y - 20.0).max(10.0);
3135
3136 egui::Area::new(egui::Id::new("scrollbar_mark_tooltip"))
3138 .order(egui::Order::Tooltip)
3139 .fixed_pos(egui::pos2(tooltip_x, tooltip_y))
3140 .show(ctx, |ui| {
3141 ui.set_min_width(150.0);
3142 egui::Frame::NONE
3143 .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 240))
3144 .inner_margin(egui::Margin::same(8))
3145 .corner_radius(4.0)
3146 .stroke(egui::Stroke::new(
3147 1.0,
3148 egui::Color32::from_rgb(80, 80, 80),
3149 ))
3150 .show(ui, |ui| {
3151 ui.set_min_width(140.0);
3152 ui.style_mut().visuals.override_text_color =
3153 Some(egui::Color32::from_rgb(220, 220, 220));
3154 ui.label(
3155 egui::RichText::new(&tooltip_text)
3156 .monospace()
3157 .size(12.0),
3158 );
3159 });
3160 });
3161 }
3162
3163 pending_tab_action = self.tab_bar_ui.render(
3165 ctx,
3166 &self.tab_manager,
3167 &self.config,
3168 &self.profile_manager,
3169 );
3170
3171 self.tmux_status_bar_ui.render(
3173 ctx,
3174 &self.config,
3175 self.tmux_session.as_ref(),
3176 self.tmux_session_name.as_deref(),
3177 );
3178
3179 if let Some(ref session_vars) = status_bar_session_vars {
3181 self.status_bar_ui.render(
3182 ctx,
3183 &self.config,
3184 session_vars,
3185 self.is_fullscreen,
3186 );
3187 }
3188
3189 self.help_ui.show(ctx);
3194
3195 pending_clipboard_action = self.clipboard_history_ui.show(ctx);
3197
3198 pending_command_history_action = self.command_history_ui.show(ctx);
3200
3201 pending_paste_special_action = self.paste_special_ui.show(ctx);
3203
3204 pending_search_action = self.search_ui.show(ctx, visible_lines, scrollback_len);
3206
3207 pending_inspector_action = self.ai_inspector.show(ctx, &self.available_agents);
3209
3210 let tmux_path = self.config.resolve_tmux_path();
3212 pending_session_picker_action =
3213 self.tmux_session_picker_ui.show(ctx, &tmux_path);
3214
3215 pending_shader_install_response = self.shader_install_ui.show(ctx);
3217
3218 pending_integrations_response = self.integrations_ui.show(ctx);
3220
3221 pending_close_confirm_action = self.close_confirmation_ui.show(ctx);
3223
3224 pending_quit_confirm_action = self.quit_confirmation_ui.show(ctx);
3226
3227 pending_remote_install_action = self.remote_shell_install_ui.show(ctx);
3229
3230 pending_ssh_connect_action = self.ssh_connect_ui.show(ctx);
3232
3233 pending_profile_drawer_action = self.profile_drawer_ui.render(
3235 ctx,
3236 &self.profile_manager,
3237 &self.config,
3238 false, );
3240
3241 if let (Some(snap), Some(size)) = (&progress_snapshot, window_size_for_badge) {
3243 render_progress_bars(
3244 ctx,
3245 snap,
3246 &self.config,
3247 size.width as f32,
3248 size.height as f32,
3249 );
3250 }
3251
3252 if !pane_identify_bounds.is_empty() {
3254 for (index, bounds) in &pane_identify_bounds {
3255 let center_x = bounds.x + bounds.width / 2.0;
3256 let center_y = bounds.y + bounds.height / 2.0;
3257 egui::Area::new(egui::Id::new(format!("pane_identify_{}", index)))
3258 .fixed_pos(egui::pos2(center_x - 30.0, center_y - 30.0))
3259 .order(egui::Order::Foreground)
3260 .interactable(false)
3261 .show(ctx, |ui| {
3262 egui::Frame::NONE
3263 .fill(egui::Color32::from_rgba_unmultiplied(0, 0, 0, 200))
3264 .inner_margin(egui::Margin::symmetric(16, 8))
3265 .corner_radius(8.0)
3266 .stroke(egui::Stroke::new(
3267 2.0,
3268 egui::Color32::from_rgb(100, 200, 255),
3269 ))
3270 .show(ui, |ui| {
3271 ui.label(
3272 egui::RichText::new(format!("Pane {}", index))
3273 .monospace()
3274 .size(28.0)
3275 .color(egui::Color32::from_rgb(100, 200, 255)),
3276 );
3277 });
3278 });
3279 }
3280 }
3281
3282 crate::app::file_transfers::render_file_transfer_overlay(
3284 &self.file_transfer_state,
3285 ctx,
3286 );
3287
3288 if let (Some(badge), Some(size)) = (&badge_state, window_size_for_badge) {
3290 render_badge(ctx, badge, size.width as f32, size.height as f32);
3291 }
3292 });
3293
3294 egui_state.handle_platform_output(
3297 self.window.as_ref().unwrap(),
3298 egui_output.platform_output.clone(),
3299 );
3300
3301 Some((egui_output, egui_ctx))
3302 } else {
3303 None
3304 };
3305
3306 if !self.egui_initialized && egui_data.is_some() {
3308 self.egui_initialized = true;
3309 }
3310
3311 let debug_egui_time = egui_start.elapsed();
3315
3316 let avg_frame_time = if !self.debug.frame_times.is_empty() {
3318 self.debug.frame_times.iter().sum::<std::time::Duration>()
3319 / self.debug.frame_times.len() as u32
3320 } else {
3321 std::time::Duration::ZERO
3322 };
3323 let fps = if avg_frame_time.as_secs_f64() > 0.0 {
3324 1.0 / avg_frame_time.as_secs_f64()
3325 } else {
3326 0.0
3327 };
3328
3329 self.debug.fps_value = fps;
3331
3332 if self.debug.frame_times.len() >= 60 {
3334 let (cache_gen, cache_has_cells) = self
3335 .tab_manager
3336 .active_tab()
3337 .map(|t| (t.cache.generation, t.cache.cells.is_some()))
3338 .unwrap_or((0, false));
3339 log::info!(
3340 "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={}",
3341 fps,
3342 avg_frame_time.as_secs_f64() * 1000.0,
3343 self.debug.cell_gen_time.as_secs_f64() * 1000.0,
3344 if self.debug.cache_hit { "HIT" } else { "MISS" },
3345 debug_url_detect_time.as_secs_f64() * 1000.0,
3346 debug_anim_time.as_secs_f64() * 1000.0,
3347 debug_graphics_time.as_secs_f64() * 1000.0,
3348 debug_egui_time.as_secs_f64() * 1000.0,
3349 debug_update_cells_time.as_secs_f64() * 1000.0,
3350 debug_actual_render_time.as_secs_f64() * 1000.0,
3351 self.debug.render_time.as_secs_f64() * 1000.0,
3352 cells.len(),
3353 cache_gen,
3354 if cache_has_cells { "YES" } else { "NO" }
3355 );
3356 }
3357
3358 let actual_render_start = std::time::Instant::now();
3360 let sizing = RendererSizing {
3364 size: renderer.size(),
3365 content_offset_y: renderer.content_offset_y(),
3366 content_offset_x: renderer.content_offset_x(),
3367 content_inset_bottom: renderer.content_inset_bottom(),
3368 content_inset_right: renderer.content_inset_right(),
3369 cell_width: renderer.cell_width(),
3370 cell_height: renderer.cell_height(),
3371 padding: renderer.window_padding(),
3372 status_bar_height: (status_bar_height + custom_status_bar_height)
3373 * renderer.scale_factor(),
3374 scale_factor: renderer.scale_factor(),
3375 };
3376
3377 let (has_pane_manager, pane_count) = self
3383 .tab_manager
3384 .active_tab()
3385 .and_then(|t| t.pane_manager.as_ref())
3386 .map(|pm| (pm.pane_count() > 0, pm.pane_count()))
3387 .unwrap_or((false, 0));
3388
3389 crate::debug_trace!(
3390 "RENDER",
3391 "has_pane_manager={}, pane_count={}",
3392 has_pane_manager,
3393 pane_count
3394 );
3395
3396 let pane_0_bg: Option<crate::pane::PaneBackground> = None;
3399
3400 let render_result = if has_pane_manager {
3401 let content_width = sizing.size.width as f32
3403 - sizing.padding * 2.0
3404 - sizing.content_offset_x
3405 - sizing.content_inset_right;
3406 let content_height = sizing.size.height as f32
3407 - sizing.content_offset_y
3408 - sizing.content_inset_bottom
3409 - sizing.padding
3410 - sizing.status_bar_height;
3411
3412 #[allow(clippy::type_complexity)]
3414 let pane_render_data: Option<(
3415 Vec<PaneRenderData>,
3416 Vec<crate::pane::DividerRect>,
3417 Vec<PaneTitleInfo>,
3418 Option<PaneViewport>,
3419 )> = {
3420 let tab = self.tab_manager.active_tab_mut();
3421 if let Some(tab) = tab {
3422 if let Some(pm) = &mut tab.pane_manager {
3423 let bounds = crate::pane::PaneBounds::new(
3425 sizing.padding + sizing.content_offset_x,
3426 sizing.content_offset_y,
3427 content_width,
3428 content_height,
3429 );
3430 pm.set_bounds(bounds);
3431
3432 let title_height_offset = if self.config.show_pane_titles {
3435 self.config.pane_title_height * sizing.scale_factor
3436 } else {
3437 0.0
3438 };
3439
3440 pm.resize_all_terminals_with_padding(
3443 sizing.cell_width,
3444 sizing.cell_height,
3445 effective_pane_padding * sizing.scale_factor,
3446 title_height_offset,
3447 );
3448
3449 let focused_pane_id = pm.focused_pane_id();
3451 let all_pane_ids: Vec<_> =
3452 pm.all_panes().iter().map(|p| p.id).collect();
3453 let dividers = pm.get_dividers();
3454
3455 let pane_bg_opacity = self.config.pane_background_opacity;
3456 let inactive_opacity = if self.config.dim_inactive_panes {
3457 self.config.inactive_pane_opacity
3458 } else {
3459 1.0
3460 };
3461 let cursor_opacity = self.cursor_opacity;
3462
3463 let show_titles = self.config.show_pane_titles;
3466 let title_height = self.config.pane_title_height * sizing.scale_factor;
3467 let title_position = self.config.pane_title_position;
3468 let title_text_color = [
3469 self.config.pane_title_color[0] as f32 / 255.0,
3470 self.config.pane_title_color[1] as f32 / 255.0,
3471 self.config.pane_title_color[2] as f32 / 255.0,
3472 ];
3473 let title_bg_color = [
3474 self.config.pane_title_bg_color[0] as f32 / 255.0,
3475 self.config.pane_title_bg_color[1] as f32 / 255.0,
3476 self.config.pane_title_bg_color[2] as f32 / 255.0,
3477 ];
3478
3479 let mut pane_data = Vec::new();
3480 let mut pane_titles = Vec::new();
3481 let mut focused_viewport: Option<PaneViewport> = None;
3482
3483 for pane_id in &all_pane_ids {
3484 if let Some(pane) = pm.get_pane(*pane_id) {
3485 let is_focused = Some(*pane_id) == focused_pane_id;
3486 let bounds = pane.bounds;
3487
3488 let (viewport_y, viewport_height) = if show_titles {
3490 use crate::config::PaneTitlePosition;
3491 match title_position {
3492 PaneTitlePosition::Top => (
3493 bounds.y + title_height,
3494 (bounds.height - title_height).max(0.0),
3495 ),
3496 PaneTitlePosition::Bottom => {
3497 (bounds.y, (bounds.height - title_height).max(0.0))
3498 }
3499 }
3500 } else {
3501 (bounds.y, bounds.height)
3502 };
3503
3504 let physical_pane_padding =
3507 effective_pane_padding * sizing.scale_factor;
3508 let viewport = PaneViewport::with_padding(
3509 bounds.x,
3510 viewport_y,
3511 bounds.width,
3512 viewport_height,
3513 is_focused,
3514 if is_focused {
3515 pane_bg_opacity
3516 } else {
3517 pane_bg_opacity * inactive_opacity
3518 },
3519 physical_pane_padding,
3520 );
3521
3522 if is_focused {
3523 focused_viewport = Some(viewport);
3524 }
3525
3526 if show_titles {
3528 use crate::config::PaneTitlePosition;
3529 let title_y = match title_position {
3530 PaneTitlePosition::Top => bounds.y,
3531 PaneTitlePosition::Bottom => {
3532 bounds.y + bounds.height - title_height
3533 }
3534 };
3535 pane_titles.push(PaneTitleInfo {
3536 x: bounds.x,
3537 y: title_y,
3538 width: bounds.width,
3539 height: title_height,
3540 title: pane.get_title(),
3541 focused: is_focused,
3542 text_color: title_text_color,
3543 bg_color: title_bg_color,
3544 });
3545 }
3546
3547 let cells = if let Ok(term) = pane.terminal.try_lock() {
3548 let scroll_offset = pane.scroll_state.offset;
3549 let selection =
3550 pane.mouse.selection.map(|sel| sel.normalized());
3551 let rectangular = pane
3552 .mouse
3553 .selection
3554 .map(|sel| sel.mode == SelectionMode::Rectangular)
3555 .unwrap_or(false);
3556 term.get_cells_with_scrollback(
3557 scroll_offset,
3558 selection,
3559 rectangular,
3560 None,
3561 )
3562 } else {
3563 Vec::new()
3564 };
3565
3566 let need_marks = self.config.scrollbar_command_marks
3567 || self.config.command_separator_enabled;
3568 let (marks, pane_scrollback_len) = if need_marks {
3569 if let Ok(mut term) = pane.terminal.try_lock() {
3570 let sb_len = term.scrollback_len();
3572 term.update_scrollback_metadata(sb_len, 0);
3573 (term.scrollback_marks(), sb_len)
3574 } else {
3575 (Vec::new(), 0)
3576 }
3577 } else {
3578 (Vec::new(), 0)
3579 };
3580 let pane_scroll_offset = pane.scroll_state.offset;
3581
3582 let pane_background = if all_pane_ids.len() > 1
3584 && pane.background().has_image()
3585 {
3586 Some(pane.background().clone())
3587 } else {
3588 None
3589 };
3590
3591 let cursor_pos = if let Ok(term) = pane.terminal.try_lock() {
3592 if term.is_cursor_visible() {
3593 Some(term.cursor_position())
3594 } else {
3595 None
3596 }
3597 } else {
3598 None
3599 };
3600
3601 let content_width = (bounds.width
3604 - physical_pane_padding * 2.0)
3605 .max(sizing.cell_width);
3606 let content_height = (viewport_height
3607 - physical_pane_padding * 2.0)
3608 .max(sizing.cell_height);
3609 let cols = (content_width / sizing.cell_width).floor() as usize;
3610 let rows =
3611 (content_height / sizing.cell_height).floor() as usize;
3612 let cols = cols.max(1);
3613 let rows = rows.max(1);
3614
3615 pane_data.push((
3616 viewport,
3617 cells,
3618 (cols, rows),
3619 cursor_pos,
3620 if is_focused { cursor_opacity } else { 0.0 },
3621 marks,
3622 pane_scrollback_len,
3623 pane_scroll_offset,
3624 pane_background,
3625 ));
3626 }
3627 }
3628
3629 Some((pane_data, dividers, pane_titles, focused_viewport))
3630 } else {
3631 None
3632 }
3633 } else {
3634 None
3635 }
3636 };
3637
3638 if let Some((pane_data, dividers, pane_titles, focused_viewport)) = pane_render_data
3639 {
3640 let hovered_divider_index = self
3642 .tab_manager
3643 .active_tab()
3644 .and_then(|t| t.mouse.hovered_divider_index);
3645
3646 Self::render_split_panes_with_data(
3648 renderer,
3649 pane_data,
3650 dividers,
3651 pane_titles,
3652 focused_viewport,
3653 &self.config,
3654 egui_data,
3655 hovered_divider_index,
3656 )
3657 } else {
3658 renderer.render(egui_data, false, show_scrollbar, pane_0_bg.as_ref())
3660 }
3661 } else {
3662 renderer.render(egui_data, false, show_scrollbar, pane_0_bg.as_ref())
3664 };
3665
3666 match render_result {
3667 Ok(rendered) => {
3668 if !rendered {
3669 log::trace!("Skipped rendering - no changes");
3670 }
3671 }
3672 Err(e) => {
3673 if let Some(surface_error) = e.downcast_ref::<SurfaceError>() {
3676 match surface_error {
3677 SurfaceError::Outdated | SurfaceError::Lost => {
3678 log::warn!(
3679 "Surface error detected ({:?}), reconfiguring...",
3680 surface_error
3681 );
3682 self.force_surface_reconfigure();
3683 }
3684 SurfaceError::Timeout => {
3685 log::warn!("Surface timeout, will retry next frame");
3686 if let Some(window) = &self.window {
3687 window.request_redraw();
3688 }
3689 }
3690 SurfaceError::OutOfMemory => {
3691 log::error!("Surface out of memory: {:?}", surface_error);
3692 }
3693 _ => {
3694 log::error!("Surface error: {:?}", surface_error);
3695 }
3696 }
3697 } else {
3698 log::error!("Render error: {}", e);
3699 }
3700 }
3701 }
3702 debug_actual_render_time = actual_render_start.elapsed();
3703 let _ = debug_actual_render_time;
3704
3705 self.debug.render_time = render_start.elapsed();
3706 }
3707
3708 self.sync_ai_inspector_width();
3712
3713 match pending_tab_action {
3716 TabBarAction::SwitchTo(id) => {
3717 self.tab_manager.switch_to(id);
3718 if let Some(renderer) = &mut self.renderer {
3720 renderer.clear_all_cells();
3721 }
3722 if let Some(tab) = self.tab_manager.active_tab_mut() {
3723 tab.cache.cells = None;
3724 }
3725 self.needs_redraw = true;
3726 if let Some(window) = &self.window {
3727 window.request_redraw();
3728 }
3729 }
3730 TabBarAction::Close(id) => {
3731 self.tab_manager.switch_to(id);
3735 let was_last = self.close_current_tab();
3736 if was_last {
3737 self.is_shutting_down = true;
3738 }
3739 if let Some(window) = &self.window {
3740 window.request_redraw();
3741 }
3742 }
3743 TabBarAction::NewTab => {
3744 self.new_tab();
3745 if let Some(window) = &self.window {
3746 window.request_redraw();
3747 }
3748 }
3749 TabBarAction::SetColor(id, color) => {
3750 if let Some(tab) = self.tab_manager.get_tab_mut(id) {
3751 tab.set_custom_color(color);
3752 log::info!(
3753 "Set custom color for tab {}: RGB({}, {}, {})",
3754 id,
3755 color[0],
3756 color[1],
3757 color[2]
3758 );
3759 }
3760 if let Some(window) = &self.window {
3761 window.request_redraw();
3762 }
3763 }
3764 TabBarAction::ClearColor(id) => {
3765 if let Some(tab) = self.tab_manager.get_tab_mut(id) {
3766 tab.clear_custom_color();
3767 log::info!("Cleared custom color for tab {}", id);
3768 }
3769 if let Some(window) = &self.window {
3770 window.request_redraw();
3771 }
3772 }
3773 TabBarAction::Reorder(id, target_index) => {
3774 if self.tab_manager.move_tab_to_index(id, target_index) {
3775 self.needs_redraw = true;
3776 if let Some(window) = &self.window {
3777 window.request_redraw();
3778 }
3779 }
3780 }
3781 TabBarAction::NewTabWithProfile(profile_id) => {
3782 self.open_profile(profile_id);
3783 if let Some(window) = &self.window {
3784 window.request_redraw();
3785 }
3786 }
3787 TabBarAction::Duplicate(id) => {
3788 self.duplicate_tab_by_id(id);
3789 if let Some(window) = &self.window {
3790 window.request_redraw();
3791 }
3792 }
3793 TabBarAction::None => {}
3794 }
3795
3796 match pending_clipboard_action {
3799 ClipboardHistoryAction::Paste(content) => {
3800 self.paste_text(&content);
3801 }
3802 ClipboardHistoryAction::ClearAll => {
3803 if let Some(tab) = self.tab_manager.active_tab()
3804 && let Ok(term) = tab.terminal.try_lock()
3805 {
3806 term.clear_all_clipboard_history();
3807 log::info!("Cleared all clipboard history");
3808 }
3809 self.clipboard_history_ui.update_entries(Vec::new());
3810 }
3811 ClipboardHistoryAction::ClearSlot(slot) => {
3812 if let Some(tab) = self.tab_manager.active_tab()
3813 && let Ok(term) = tab.terminal.try_lock()
3814 {
3815 term.clear_clipboard_history(slot);
3816 log::info!("Cleared clipboard history for slot {:?}", slot);
3817 }
3818 }
3819 ClipboardHistoryAction::None => {}
3820 }
3821
3822 match pending_command_history_action {
3824 CommandHistoryAction::Insert(command) => {
3825 self.paste_text(&command);
3826 log::info!(
3827 "Inserted command from history: {}",
3828 &command[..command.len().min(60)]
3829 );
3830 }
3831 CommandHistoryAction::None => {}
3832 }
3833
3834 match pending_close_confirm_action {
3836 CloseConfirmAction::Close { tab_id, pane_id } => {
3837 if let Some(pane_id) = pane_id {
3839 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
3841 && let Some(pm) = tab.pane_manager_mut()
3842 {
3843 pm.close_pane(pane_id);
3844 log::info!("Force-closed pane {} in tab {}", pane_id, tab_id);
3845 }
3846 } else {
3847 self.tab_manager.close_tab(tab_id);
3849 log::info!("Force-closed tab {}", tab_id);
3850 }
3851 self.needs_redraw = true;
3852 if let Some(window) = &self.window {
3853 window.request_redraw();
3854 }
3855 }
3856 CloseConfirmAction::Cancel => {
3857 log::debug!("Close confirmation cancelled");
3859 }
3860 CloseConfirmAction::None => {}
3861 }
3862
3863 match pending_quit_confirm_action {
3865 QuitConfirmAction::Quit => {
3866 log::info!("Quit confirmed by user");
3868 self.perform_shutdown();
3869 }
3870 QuitConfirmAction::Cancel => {
3871 log::debug!("Quit confirmation cancelled");
3872 }
3873 QuitConfirmAction::None => {}
3874 }
3875
3876 match pending_remote_install_action {
3878 RemoteShellInstallAction::Install => {
3879 let command = RemoteShellInstallUI::install_command();
3881 if let Some(tab) = self.tab_manager.active_tab()
3882 && let Ok(term) = tab.terminal.try_lock()
3883 {
3884 let _ = term.write_str(&format!("{}\r", command));
3885 }
3886 }
3887 RemoteShellInstallAction::Cancel => {
3888 }
3890 RemoteShellInstallAction::None => {}
3891 }
3892
3893 match pending_ssh_connect_action {
3895 SshConnectAction::Connect {
3896 host,
3897 profile_override: _,
3898 } => {
3899 let args = host.ssh_args();
3901 let ssh_cmd = format!("ssh {}\n", args.join(" "));
3902 if let Some(tab) = self.tab_manager.active_tab()
3903 && let Ok(term) = tab.terminal.try_lock()
3904 {
3905 let _ = term.write_str(&ssh_cmd);
3906 }
3907 log::info!(
3908 "SSH Quick Connect: connecting to {}",
3909 host.connection_string()
3910 );
3911 if let Some(window) = &self.window {
3912 window.request_redraw();
3913 }
3914 }
3915 SshConnectAction::Cancel => {
3916 if let Some(window) = &self.window {
3917 window.request_redraw();
3918 }
3919 }
3920 SshConnectAction::None => {}
3921 }
3922
3923 match pending_paste_special_action {
3925 PasteSpecialAction::Paste(content) => {
3926 self.paste_text(&content);
3927 log::debug!("Pasted transformed text ({} chars)", content.len());
3928 }
3929 PasteSpecialAction::None => {}
3930 }
3931
3932 match pending_search_action {
3934 crate::search::SearchAction::ScrollToMatch(offset) => {
3935 self.set_scroll_target(offset);
3936 self.needs_redraw = true;
3937 if let Some(window) = &self.window {
3938 window.request_redraw();
3939 }
3940 }
3941 crate::search::SearchAction::Close => {
3942 self.needs_redraw = true;
3943 if let Some(window) = &self.window {
3944 window.request_redraw();
3945 }
3946 }
3947 crate::search::SearchAction::None => {}
3948 }
3949
3950 match pending_inspector_action {
3952 InspectorAction::Close => {
3953 self.ai_inspector.open = false;
3954 self.sync_ai_inspector_width();
3955 }
3956 InspectorAction::CopyJson(json) => {
3957 if let Ok(mut clipboard) = arboard::Clipboard::new() {
3958 let _ = clipboard.set_text(json);
3959 }
3960 }
3961 InspectorAction::SaveToFile(json) => {
3962 if let Some(path) = rfd::FileDialog::new()
3963 .set_file_name(format!(
3964 "par-term-snapshot-{}.json",
3965 chrono::Local::now().format("%Y-%m-%d-%H%M%S")
3966 ))
3967 .add_filter("JSON", &["json"])
3968 .save_file()
3969 {
3970 let _ = std::fs::write(path, json);
3971 }
3972 }
3973 InspectorAction::WriteToTerminal(cmd) => {
3974 if let Some(tab) = self.tab_manager.active_tab()
3975 && let Ok(term) = tab.terminal.try_lock()
3976 {
3977 let _ = term.write(cmd.as_bytes());
3978 }
3979 }
3980 InspectorAction::RunCommandAndNotify(cmd) => {
3981 if let Some(tab) = self.tab_manager.active_tab()
3983 && let Ok(term) = tab.terminal.try_lock()
3984 {
3985 let _ = term.write(format!("{cmd}\n").as_bytes());
3986 }
3987 let history_len = self
3989 .tab_manager
3990 .active_tab()
3991 .and_then(|tab| tab.terminal.try_lock().ok())
3992 .map(|term| term.core_command_history().len())
3993 .unwrap_or(0);
3994 if let Some(agent) = &self.agent {
3996 let agent = agent.clone();
3997 let tx = self.agent_tx.clone();
3998 let terminal = self
3999 .tab_manager
4000 .active_tab()
4001 .map(|tab| tab.terminal.clone());
4002 let cmd_for_msg = cmd.clone();
4003 self.runtime.spawn(async move {
4004 let mut exit_code: Option<i32> = None;
4006 for _ in 0..300 {
4007 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
4008 if let Some(ref terminal) = terminal
4009 && let Ok(term) = terminal.try_lock()
4010 {
4011 let history = term.core_command_history();
4012 if history.len() > history_len {
4013 if let Some(last) = history.last() {
4015 exit_code = last.1;
4016 }
4017 break;
4018 }
4019 }
4020 }
4021 let exit_str = exit_code
4023 .map(|c| format!("exit code {c}"))
4024 .unwrap_or_else(|| "unknown exit code".to_string());
4025 let feedback = format!(
4026 "[System: The user executed `{cmd_for_msg}` in their terminal ({exit_str}). \
4027 The output is available through the normal terminal capture.]"
4028 );
4029 let content = vec![par_term_acp::ContentBlock::Text {
4030 text: feedback,
4031 }];
4032 let agent = agent.lock().await;
4033 let _ = agent.send_prompt(content).await;
4034 if let Some(tx) = tx {
4035 let _ = tx.send(par_term_acp::AgentMessage::PromptComplete);
4036 }
4037 });
4038 }
4039 self.needs_redraw = true;
4040 }
4041 InspectorAction::ConnectAgent(identity) => {
4042 self.connect_agent(&identity);
4043 }
4044 InspectorAction::DisconnectAgent => {
4045 if let Some(agent) = self.agent.take() {
4046 self.runtime.spawn(async move {
4047 let mut agent = agent.lock().await;
4048 agent.disconnect().await;
4049 });
4050 }
4051 self.agent_rx = None;
4052 self.agent_tx = None;
4053 self.agent_client = None;
4054 self.ai_inspector.agent_status = AgentStatus::Disconnected;
4055 self.needs_redraw = true;
4056 }
4057 InspectorAction::SendPrompt(text) => {
4058 self.ai_inspector.chat.add_user_message(text.clone());
4059 if let Some(agent) = &self.agent {
4060 let agent = agent.clone();
4061 let mut prompt_text = String::new();
4063
4064 if !self.ai_inspector.chat.system_prompt_sent {
4067 self.ai_inspector.chat.system_prompt_sent = true;
4068 prompt_text.push_str(crate::ai_inspector::chat::AGENT_SYSTEM_GUIDANCE);
4069 }
4070
4071 if crate::ai_inspector::shader_context::should_inject_shader_context(
4073 &text,
4074 &self.config,
4075 ) {
4076 prompt_text.push_str(
4077 &crate::ai_inspector::shader_context::build_shader_context(
4078 &self.config,
4079 ),
4080 );
4081 }
4082
4083 prompt_text.push_str(&text);
4084
4085 let content = vec![par_term_acp::ContentBlock::Text { text: prompt_text }];
4086 let tx = self.agent_tx.clone();
4087 self.runtime.spawn(async move {
4088 let agent = agent.lock().await;
4089 let _ = agent.send_prompt(content).await;
4090 if let Some(tx) = tx {
4093 let _ = tx.send(AgentMessage::PromptComplete);
4094 }
4095 });
4096 }
4097 self.needs_redraw = true;
4098 }
4099 InspectorAction::SetTerminalAccess(enabled) => {
4100 self.config.ai_inspector_agent_terminal_access = enabled;
4101 self.needs_redraw = true;
4102 }
4103 InspectorAction::RespondPermission {
4104 request_id,
4105 option_id,
4106 cancelled,
4107 } => {
4108 if let Some(client) = &self.agent_client {
4109 let client = client.clone();
4110 let action = if cancelled { "cancelled" } else { "selected" };
4111 log::info!("ACP: sending permission response id={request_id} action={action}");
4112 self.runtime.spawn(async move {
4113 use par_term_acp::{PermissionOutcome, RequestPermissionResponse};
4114 let outcome = if cancelled {
4115 PermissionOutcome {
4116 outcome: "cancelled".to_string(),
4117 option_id: None,
4118 }
4119 } else {
4120 PermissionOutcome {
4121 outcome: "selected".to_string(),
4122 option_id: Some(option_id),
4123 }
4124 };
4125 let result = RequestPermissionResponse { outcome };
4126 if let Err(e) = client
4127 .respond(
4128 request_id,
4129 Some(serde_json::to_value(&result).unwrap()),
4130 None,
4131 )
4132 .await
4133 {
4134 log::error!("ACP: failed to send permission response: {e}");
4135 }
4136 });
4137 } else {
4138 log::error!(
4139 "ACP: cannot send permission response id={request_id} — agent_client is None!"
4140 );
4141 }
4142 for msg in &mut self.ai_inspector.chat.messages {
4144 if let ChatMessage::Permission {
4145 request_id: rid,
4146 resolved,
4147 ..
4148 } = msg
4149 && *rid == request_id
4150 {
4151 *resolved = true;
4152 break;
4153 }
4154 }
4155 self.needs_redraw = true;
4156 }
4157 InspectorAction::SetAgentMode(mode_id) => {
4158 let is_yolo = mode_id == "bypassPermissions";
4159 self.config.ai_inspector_auto_approve = is_yolo;
4160 if let Some(agent) = &self.agent {
4161 let agent = agent.clone();
4162 self.runtime.spawn(async move {
4163 let agent = agent.lock().await;
4164 agent
4165 .auto_approve
4166 .store(is_yolo, std::sync::atomic::Ordering::Relaxed);
4167 if let Err(e) = agent.set_mode(&mode_id).await {
4168 log::error!("ACP: failed to set mode '{mode_id}': {e}");
4169 }
4170 });
4171 }
4172 self.needs_redraw = true;
4173 }
4174 InspectorAction::None => {}
4175 }
4176
4177 match pending_session_picker_action {
4180 SessionPickerAction::Attach(session_name) => {
4181 crate::debug_info!(
4182 "TMUX",
4183 "Session picker: attaching to '{}' via gateway",
4184 session_name
4185 );
4186 if let Err(e) = self.attach_tmux_gateway(&session_name) {
4187 log::error!("Failed to attach to tmux session '{}': {}", session_name, e);
4188 self.show_toast(format!("Failed to attach: {}", e));
4189 } else {
4190 crate::debug_info!("TMUX", "Gateway initiated for session '{}'", session_name);
4191 self.show_toast(format!("Connecting to session '{}'...", session_name));
4192 }
4193 self.needs_redraw = true;
4194 }
4195 SessionPickerAction::CreateNew(name) => {
4196 crate::debug_info!(
4197 "TMUX",
4198 "Session picker: creating new session {:?} via gateway",
4199 name
4200 );
4201 if let Err(e) = self.initiate_tmux_gateway(name.as_deref()) {
4202 log::error!("Failed to create tmux session: {}", e);
4203 crate::debug_error!("TMUX", "Failed to initiate gateway: {}", e);
4204 self.show_toast(format!("Failed to create session: {}", e));
4205 } else {
4206 let msg = match name {
4207 Some(ref n) => format!("Creating session '{}'...", n),
4208 None => "Creating new tmux session...".to_string(),
4209 };
4210 crate::debug_info!("TMUX", "Gateway initiated: {}", msg);
4211 self.show_toast(msg);
4212 }
4213 self.needs_redraw = true;
4214 }
4215 SessionPickerAction::None => {}
4216 }
4217
4218 if let Some(ref rx) = self.shader_install_receiver
4220 && let Ok(result) = rx.try_recv()
4221 {
4222 match result {
4223 Ok(count) => {
4224 log::info!("Successfully installed {} shaders", count);
4225 self.shader_install_ui
4226 .set_success(&format!("Installed {} shaders!", count));
4227
4228 self.config.shader_install_prompt = ShaderInstallPrompt::Installed;
4230 if let Err(e) = self.config.save() {
4231 log::error!("Failed to save config after shader install: {}", e);
4232 }
4233 }
4234 Err(e) => {
4235 log::error!("Failed to install shaders: {}", e);
4236 self.shader_install_ui.set_error(&e);
4237 }
4238 }
4239 self.shader_install_receiver = None;
4240 self.needs_redraw = true;
4241 }
4242
4243 match pending_shader_install_response {
4245 ShaderInstallResponse::Install => {
4246 log::info!("User requested shader installation");
4247 self.shader_install_ui
4248 .set_installing("Downloading shaders...");
4249 self.needs_redraw = true;
4250
4251 let (tx, rx) = std::sync::mpsc::channel();
4253 self.shader_install_receiver = Some(rx);
4254
4255 std::thread::spawn(move || {
4256 let result = crate::shader_install_ui::install_shaders_headless();
4257 let _ = tx.send(result);
4258 });
4259
4260 if let Some(window) = &self.window {
4262 window.request_redraw();
4263 }
4264 }
4265 ShaderInstallResponse::Never => {
4266 log::info!("User declined shader installation (never ask again)");
4267 self.shader_install_ui.hide();
4268
4269 self.config.shader_install_prompt = ShaderInstallPrompt::Never;
4271 if let Err(e) = self.config.save() {
4272 log::error!("Failed to save config after declining shaders: {}", e);
4273 }
4274 }
4275 ShaderInstallResponse::Later => {
4276 log::info!("User deferred shader installation");
4277 self.shader_install_ui.hide();
4278 }
4280 ShaderInstallResponse::None => {}
4281 }
4282
4283 self.handle_integrations_response(&pending_integrations_response);
4285
4286 match pending_profile_drawer_action {
4288 ProfileDrawerAction::OpenProfile(id) => {
4289 self.open_profile(id);
4290 }
4291 ProfileDrawerAction::ManageProfiles => {
4292 self.open_settings_window_requested = true;
4294 self.open_settings_profiles_tab = true;
4295 }
4296 ProfileDrawerAction::None => {}
4297 }
4298
4299 let absolute_total = absolute_start.elapsed();
4300 if absolute_total.as_millis() > 10 {
4301 log::debug!(
4302 "TIMING: AbsoluteTotal={:.2}ms (from function start to end)",
4303 absolute_total.as_secs_f64() * 1000.0
4304 );
4305 }
4306 }
4307
4308 #[allow(clippy::too_many_arguments)]
4310 fn render_split_panes_with_data(
4311 renderer: &mut Renderer,
4312 pane_data: Vec<PaneRenderData>,
4313 dividers: Vec<crate::pane::DividerRect>,
4314 pane_titles: Vec<PaneTitleInfo>,
4315 focused_viewport: Option<PaneViewport>,
4316 config: &Config,
4317 egui_data: Option<(egui::FullOutput, &egui::Context)>,
4318 hovered_divider_index: Option<usize>,
4319 ) -> Result<bool> {
4320 let mut pane_render_infos: Vec<PaneRenderInfo> = Vec::new();
4322 let mut leaked_cells: Vec<*mut [crate::cell_renderer::Cell]> = Vec::new();
4323
4324 for (
4325 viewport,
4326 cells,
4327 grid_size,
4328 cursor_pos,
4329 cursor_opacity,
4330 marks,
4331 scrollback_len,
4332 scroll_offset,
4333 pane_background,
4334 ) in pane_data
4335 {
4336 let cells_boxed = cells.into_boxed_slice();
4337 let cells_ptr = Box::into_raw(cells_boxed);
4338 leaked_cells.push(cells_ptr);
4339
4340 pane_render_infos.push(PaneRenderInfo {
4341 viewport,
4342 cells: unsafe { &*cells_ptr },
4344 grid_size,
4345 cursor_pos,
4346 cursor_opacity,
4347 show_scrollbar: false,
4348 marks,
4349 scrollback_len,
4350 scroll_offset,
4351 background: pane_background,
4352 });
4353 }
4354
4355 let divider_render_infos: Vec<DividerRenderInfo> = dividers
4357 .iter()
4358 .enumerate()
4359 .map(|(i, d)| DividerRenderInfo::from_rect(d, hovered_divider_index == Some(i)))
4360 .collect();
4361
4362 let divider_settings = PaneDividerSettings {
4364 divider_color: [
4365 config.pane_divider_color[0] as f32 / 255.0,
4366 config.pane_divider_color[1] as f32 / 255.0,
4367 config.pane_divider_color[2] as f32 / 255.0,
4368 ],
4369 hover_color: [
4370 config.pane_divider_hover_color[0] as f32 / 255.0,
4371 config.pane_divider_hover_color[1] as f32 / 255.0,
4372 config.pane_divider_hover_color[2] as f32 / 255.0,
4373 ],
4374 show_focus_indicator: config.pane_focus_indicator,
4375 focus_color: [
4376 config.pane_focus_color[0] as f32 / 255.0,
4377 config.pane_focus_color[1] as f32 / 255.0,
4378 config.pane_focus_color[2] as f32 / 255.0,
4379 ],
4380 focus_width: config.pane_focus_width * renderer.scale_factor(),
4381 divider_style: config.pane_divider_style,
4382 };
4383
4384 let result = renderer.render_split_panes(
4386 &pane_render_infos,
4387 ÷r_render_infos,
4388 &pane_titles,
4389 focused_viewport.as_ref(),
4390 ÷r_settings,
4391 egui_data,
4392 false,
4393 );
4394
4395 for ptr in leaked_cells {
4397 let _ = unsafe { Box::from_raw(ptr) };
4399 }
4400
4401 result
4402 }
4403
4404 fn handle_integrations_response(&mut self, response: &IntegrationsResponse) {
4406 if !response.install_shaders
4408 && !response.install_shell_integration
4409 && !response.skipped
4410 && !response.never_ask
4411 && !response.closed
4412 && response.shader_conflict_action.is_none()
4413 {
4414 return;
4415 }
4416
4417 let current_version = env!("CARGO_PKG_VERSION").to_string();
4418
4419 let mut install_shaders = false;
4421 let mut install_shell_integration = false;
4422 let mut force_overwrite_modified_shaders = false;
4423 let mut triggered_install = false;
4424
4425 if let Some(action) = response.shader_conflict_action {
4427 triggered_install = true;
4428 install_shaders = self.integrations_ui.pending_install_shaders;
4429 install_shell_integration = self.integrations_ui.pending_install_shell_integration;
4430
4431 match action {
4432 crate::integrations_ui::ShaderConflictAction::Overwrite => {
4433 force_overwrite_modified_shaders = true;
4434 }
4435 crate::integrations_ui::ShaderConflictAction::SkipModified => {
4436 force_overwrite_modified_shaders = false;
4437 }
4438 crate::integrations_ui::ShaderConflictAction::Cancel => {
4439 self.integrations_ui.awaiting_shader_overwrite = false;
4441 self.integrations_ui.shader_conflicts.clear();
4442 self.integrations_ui.pending_install_shaders = false;
4443 self.integrations_ui.pending_install_shell_integration = false;
4444 self.integrations_ui.error_message = None;
4445 self.integrations_ui.success_message = None;
4446 self.needs_redraw = true;
4447 return;
4448 }
4449 }
4450
4451 self.integrations_ui.awaiting_shader_overwrite = false;
4453 self.integrations_ui.shader_conflicts.clear();
4454 self.integrations_ui.error_message = None;
4455 self.integrations_ui.success_message = None;
4456 self.integrations_ui.installing = false;
4457 } else if response.install_shaders || response.install_shell_integration {
4458 triggered_install = true;
4459 install_shaders = response.install_shaders;
4460 install_shell_integration = response.install_shell_integration;
4461
4462 if install_shaders {
4463 match crate::shader_installer::detect_modified_bundled_shaders() {
4464 Ok(conflicts) if !conflicts.is_empty() => {
4465 log::info!(
4466 "Detected {} modified bundled shaders; prompting for overwrite",
4467 conflicts.len()
4468 );
4469 self.integrations_ui.awaiting_shader_overwrite = true;
4470 self.integrations_ui.shader_conflicts = conflicts;
4471 self.integrations_ui.pending_install_shaders = install_shaders;
4472 self.integrations_ui.pending_install_shell_integration =
4473 install_shell_integration;
4474 self.integrations_ui.installing = false;
4475 self.integrations_ui.error_message = None;
4476 self.integrations_ui.success_message = None;
4477 self.needs_redraw = true;
4478 return; }
4480 Ok(_) => {}
4481 Err(e) => {
4482 log::warn!(
4483 "Unable to check existing shaders for modifications: {}. Proceeding without overwrite prompt.",
4484 e
4485 );
4486 }
4487 }
4488 }
4489 }
4490
4491 if triggered_install {
4493 log::info!(
4494 "User requested installations: shaders={}, shell_integration={}, overwrite_modified={}",
4495 install_shaders,
4496 install_shell_integration,
4497 force_overwrite_modified_shaders
4498 );
4499
4500 let mut success_parts = Vec::new();
4501 let mut error_parts = Vec::new();
4502
4503 if install_shaders {
4505 self.integrations_ui.set_installing("Installing shaders...");
4506 self.needs_redraw = true;
4507 self.request_redraw();
4508
4509 match crate::shader_installer::install_shaders_with_manifest(
4510 force_overwrite_modified_shaders,
4511 ) {
4512 Ok(result) => {
4513 log::info!(
4514 "Installed {} shader files ({} skipped, {} removed)",
4515 result.installed,
4516 result.skipped,
4517 result.removed
4518 );
4519 let detail = if result.skipped > 0 {
4520 format!("{} shaders ({} skipped)", result.installed, result.skipped)
4521 } else {
4522 format!("{} shaders", result.installed)
4523 };
4524 success_parts.push(detail);
4525 self.config.integration_versions.shaders_installed_version =
4526 Some(current_version.clone());
4527 self.config.integration_versions.shaders_prompted_version =
4528 Some(current_version.clone());
4529 }
4530 Err(e) => {
4531 log::error!("Failed to install shaders: {}", e);
4532 error_parts.push(format!("Shaders: {}", e));
4533 }
4534 }
4535 }
4536
4537 if install_shell_integration {
4539 self.integrations_ui
4540 .set_installing("Installing shell integration...");
4541 self.needs_redraw = true;
4542 self.request_redraw();
4543
4544 match crate::shell_integration_installer::install(None) {
4545 Ok(result) => {
4546 log::info!(
4547 "Installed shell integration for {}",
4548 result.shell.display_name()
4549 );
4550 success_parts.push(format!(
4551 "shell integration ({})",
4552 result.shell.display_name()
4553 ));
4554 self.config
4555 .integration_versions
4556 .shell_integration_installed_version = Some(current_version.clone());
4557 self.config
4558 .integration_versions
4559 .shell_integration_prompted_version = Some(current_version.clone());
4560 }
4561 Err(e) => {
4562 log::error!("Failed to install shell integration: {}", e);
4563 error_parts.push(format!("Shell: {}", e));
4564 }
4565 }
4566 }
4567
4568 if error_parts.is_empty() {
4570 self.integrations_ui
4571 .set_success(&format!("Installed: {}", success_parts.join(", ")));
4572 } else if success_parts.is_empty() {
4573 self.integrations_ui
4574 .set_error(&format!("Installation failed: {}", error_parts.join("; ")));
4575 } else {
4576 self.integrations_ui.set_success(&format!(
4578 "Installed: {}. Errors: {}",
4579 success_parts.join(", "),
4580 error_parts.join("; ")
4581 ));
4582 }
4583
4584 if let Err(e) = self.config.save() {
4586 log::error!("Failed to save config after integration install: {}", e);
4587 }
4588
4589 self.integrations_ui.pending_install_shaders = false;
4591 self.integrations_ui.pending_install_shell_integration = false;
4592
4593 self.needs_redraw = true;
4594 }
4595
4596 if response.skipped {
4598 log::info!("User skipped integrations dialog for this session");
4599 self.integrations_ui.hide();
4600 self.config.integration_versions.shaders_prompted_version =
4602 Some(current_version.clone());
4603 self.config
4604 .integration_versions
4605 .shell_integration_prompted_version = Some(current_version.clone());
4606 if let Err(e) = self.config.save() {
4607 log::error!("Failed to save config after skipping integrations: {}", e);
4608 }
4609 }
4610
4611 if response.never_ask {
4613 log::info!("User declined integrations (never ask again)");
4614 self.integrations_ui.hide();
4615 self.config.shader_install_prompt = ShaderInstallPrompt::Never;
4617 self.config.shell_integration_state = crate::config::InstallPromptState::Never;
4618 if let Err(e) = self.config.save() {
4619 log::error!("Failed to save config after declining integrations: {}", e);
4620 }
4621 }
4622
4623 if response.closed {
4625 self.integrations_ui.hide();
4626 }
4627 }
4628
4629 pub(crate) fn perform_shutdown(&mut self) {
4631 if self.config.startup_directory_mode == crate::config::StartupDirectoryMode::Previous
4633 && let Some(tab) = self.tab_manager.active_tab()
4634 && let Ok(term) = tab.terminal.try_lock()
4635 && let Some(cwd) = term.shell_integration_cwd()
4636 {
4637 log::info!("Saving last working directory: {}", cwd);
4638 if let Err(e) = self.config.save_last_working_directory(&cwd) {
4639 log::warn!("Failed to save last working directory: {}", e);
4640 }
4641 }
4642
4643 self.is_shutting_down = true;
4645 for tab in self.tab_manager.tabs_mut() {
4647 if let Some(task) = tab.refresh_task.take() {
4648 task.abort();
4649 }
4650 }
4651 log::info!("Refresh tasks aborted, shutdown initiated");
4652 }
4653}
4654
4655impl Drop for WindowState {
4657 fn drop(&mut self) {
4658 let t0 = std::time::Instant::now();
4659 log::info!("Shutting down window (fast path)");
4660
4661 self.status_bar_ui.signal_shutdown();
4665
4666 self.command_history.save_background();
4668
4669 self.is_shutting_down = true;
4671
4672 if let Some(ref window) = self.window {
4674 window.set_visible(false);
4675 log::info!(
4676 "Window hidden for instant visual close (+{:.1}ms)",
4677 t0.elapsed().as_secs_f64() * 1000.0
4678 );
4679 }
4680
4681 self.egui_state = None;
4683 self.egui_ctx = None;
4684
4685 let mut tabs = self.tab_manager.drain_tabs();
4687 let tab_count = tabs.len();
4688 log::info!(
4689 "Fast shutdown: draining {} tabs (+{:.1}ms)",
4690 tab_count,
4691 t0.elapsed().as_secs_f64() * 1000.0
4692 );
4693
4694 let mut terminal_arcs = Vec::new();
4699 let mut session_loggers = Vec::new();
4700
4701 for tab in &mut tabs {
4702 tab.stop_refresh_task();
4704
4705 session_loggers.push(Arc::clone(&tab.session_logger));
4707
4708 terminal_arcs.push(Arc::clone(&tab.terminal));
4710
4711 if let Some(ref mut pm) = tab.pane_manager {
4713 for pane in pm.all_panes_mut() {
4714 pane.stop_refresh_task();
4715 session_loggers.push(Arc::clone(&pane.session_logger));
4716 terminal_arcs.push(Arc::clone(&pane.terminal));
4717 pane.shutdown_fast = true;
4718 }
4719 }
4720
4721 tab.shutdown_fast = true;
4723 }
4724
4725 for arc in &terminal_arcs {
4727 if let Ok(mut term) = arc.try_lock()
4728 && term.is_running()
4729 {
4730 let _ = term.kill();
4731 }
4732 }
4733 log::info!(
4734 "Pre-killed {} terminal sessions (+{:.1}ms)",
4735 terminal_arcs.len(),
4736 t0.elapsed().as_secs_f64() * 1000.0
4737 );
4738
4739 drop(tabs);
4741 log::info!(
4742 "Tabs dropped (+{:.1}ms)",
4743 t0.elapsed().as_secs_f64() * 1000.0
4744 );
4745
4746 if !session_loggers.is_empty() {
4749 let _ = std::thread::Builder::new()
4750 .name("logger-cleanup".into())
4751 .spawn(move || {
4752 for logger_arc in session_loggers {
4753 if let Some(ref mut logger) = *logger_arc.lock() {
4754 let _ = logger.stop();
4755 }
4756 }
4757 });
4758 }
4759
4760 for (i, arc) in terminal_arcs.into_iter().enumerate() {
4767 let _ = std::thread::Builder::new()
4768 .name(format!("pty-cleanup-{}", i))
4769 .spawn(move || {
4770 let t = std::time::Instant::now();
4771 drop(arc);
4772 log::info!(
4773 "pty-cleanup-{} finished in {:.1}ms",
4774 i,
4775 t.elapsed().as_secs_f64() * 1000.0
4776 );
4777 });
4778 }
4779
4780 log::info!(
4781 "Window shutdown complete ({} tabs, main thread blocked {:.1}ms)",
4782 tab_count,
4783 t0.elapsed().as_secs_f64() * 1000.0
4784 );
4785 }
4786}