Skip to main content

par_term/app/window_state/
impl_init.rs

1//! Constructor and async initialization for `WindowState`.
2
3use super::impl_agent::merge_custom_ai_inspector_agents;
4use super::{
5    EguiState, FocusState, OverlayState, RenderLoopState, TriggerState, UpdateState, WatcherState,
6    WindowState,
7};
8use crate::badge::BadgeState;
9use crate::config::Config;
10use crate::smart_selection::SmartSelectionCache;
11use crate::status_bar::StatusBarUI;
12use crate::tab::TabManager;
13use crate::tab_bar_ui::TabBarUI;
14use anyhow::Result;
15use arc_swap::ArcSwap;
16use par_term_acp::discover_agents;
17use par_term_input::InputHandler;
18use par_term_keybindings::{KeyCombo, KeybindingRegistry};
19use std::sync::Arc;
20use tokio::runtime::Runtime;
21use winit::window::Window;
22
23impl WindowState {
24    pub(crate) fn parse_custom_action_prefix_combo(prefix_key: &str) -> Option<KeyCombo> {
25        let trimmed = prefix_key.trim();
26        if trimmed.is_empty() {
27            return None;
28        }
29
30        match par_term_keybindings::parser::parse_key_combo(trimmed) {
31            Ok(combo) => Some(combo),
32            Err(error) => {
33                log::warn!(
34                    "Invalid custom action prefix key '{}': {}",
35                    prefix_key,
36                    error
37                );
38                None
39            }
40        }
41    }
42
43    /// Create a new window state with the given configuration
44    pub fn new(config: Config, runtime: Arc<Runtime>) -> Self {
45        let keybinding_registry = KeybindingRegistry::from_config(&config.keybindings);
46        let custom_action_prefix_combo =
47            Self::parse_custom_action_prefix_combo(&config.custom_action_prefix_key);
48        let shaders_dir = Config::shaders_dir();
49        let tmux_prefix_key = crate::tmux::PrefixKey::parse(&config.tmux.tmux_prefix_key);
50
51        let mut input_handler = InputHandler::new();
52        // Initialize Option/Alt key modes from config
53        input_handler.update_option_key_modes(
54            config.input.left_option_key_mode,
55            config.input.right_option_key_mode,
56        );
57
58        // Create badge state and overlay UI before wrapping config in ArcSwap
59        let badge_state = BadgeState::new(&config);
60        let overlay_ui = crate::app::window_state::overlay_ui_state::OverlayUiState::new(&config);
61
62        // Discover available ACP agents
63        let config_dir = Config::config_dir();
64        let discovered_agents = discover_agents(&config_dir);
65        let available_agents = merge_custom_ai_inspector_agents(
66            discovered_agents,
67            &config.ai_inspector.ai_inspector_custom_agents,
68        );
69
70        Self {
71            config: ArcSwap::from(Arc::new(config)),
72            window: None,
73            renderer: None,
74            input_handler,
75            runtime,
76
77            tab_manager: TabManager::new(),
78            tab_bar_ui: TabBarUI::new(),
79            status_bar_ui: StatusBarUI::new(),
80
81            debug: crate::app::window_state::debug_state::DebugState::new(),
82            frame: crate::app::window_state::frame_state::FrameState::new(),
83
84            cursor_anim: crate::app::window_state::cursor_anim_state::CursorAnimState::default(),
85            is_fullscreen: false,
86            egui: EguiState::default(),
87            shader_state: crate::app::window_state::shader_state::ShaderState::new(shaders_dir),
88            overlay_ui,
89            agent_state: super::agent_state::AgentState::new(available_agents),
90            is_recording: false,
91            is_shutting_down: false,
92            window_index: 1, // Will be set by WindowManager when window is created
93
94            focus_state: FocusState::default(),
95
96            window_move_gate: crate::app::display_recovery::WindowMoveGate::new(),
97
98            render_loop: RenderLoopState::default(),
99
100            watcher_state: WatcherState::default(),
101
102            clipboard_image_click_guard: None,
103
104            last_osc52_clipboard: None,
105
106            overlay_state: OverlayState::default(),
107
108            keybinding_registry,
109            custom_action_prefix_combo,
110            custom_action_prefix_state: crate::tmux::PrefixState::default(),
111
112            smart_selection_cache: SmartSelectionCache::new(),
113
114            tmux_state: super::TmuxState::new(tmux_prefix_key),
115
116            broadcast_input: false,
117            pane_transfer_state: Default::default(),
118            pane_hint_select: Default::default(),
119
120            badge_state,
121
122            copy_mode: crate::copy_mode::CopyModeState::new(),
123
124            file_transfer_state: crate::app::file_transfers::FileTransferState::default(),
125
126            update_state: UpdateState::default(),
127
128            trigger_state: TriggerState::default(),
129
130            agent_commands: crate::agent_commands_store::AgentCommandStore::new(),
131
132            crash_triage: crate::crash_triage::CrashTriageState::default(),
133
134            notification_click_state: super::NotificationClickState::default(),
135
136            pending_snap_size: None,
137
138            last_workflow_context: std::sync::Arc::new(std::sync::Mutex::new(None)),
139        }
140    }
141
142    /// Format window title with optional window number
143    /// This should be used everywhere a title is set to ensure consistency
144    pub(crate) fn format_title(&self, base_title: &str) -> String {
145        if self.config.load().placement.show_window_number {
146            format!("{} [{}]", base_title, self.window_index)
147        } else {
148            base_title.to_string()
149        }
150    }
151
152    /// Extract a substring based on character columns to avoid UTF-8 slicing panics
153    pub(crate) fn extract_columns(line: &str, start_col: usize, end_col: Option<usize>) -> String {
154        let mut extracted = String::new();
155        let end_bound = end_col.unwrap_or(usize::MAX);
156
157        if start_col > end_bound {
158            return extracted;
159        }
160
161        for (idx, ch) in line.chars().enumerate() {
162            if idx > end_bound {
163                break;
164            }
165
166            if idx >= start_col {
167                extracted.push(ch);
168            }
169        }
170
171        extracted
172    }
173
174    /// Initialize the window asynchronously
175    ///
176    /// `skip_default_tab` - When `true`, skips creating the default shell tab.
177    /// Used by the "Move Tab to New Window" path to initialize a fully functional
178    /// window that starts with an empty `TabManager`, ready to receive a transferred
179    /// tab via `insert_tab_at`. All normal callers pass `false`.
180    ///
181    /// `first_tab_cwd` - Optional working directory for the first tab.
182    /// Used by arrangement restore to set the CWD before the shell spawns.
183    pub(crate) async fn initialize_async(
184        &mut self,
185        window: Window,
186        skip_default_tab: bool,
187        first_tab_cwd: Option<String>,
188    ) -> Result<()> {
189        use crate::app::window_state::renderer_init::RendererInitParams;
190
191        // Enable IME (Input Method Editor) to receive all character events including Space
192        window.set_ime_allowed(true);
193        log::debug!("IME enabled for character input");
194
195        // Detect system theme at startup and apply if auto_dark_mode is enabled
196        {
197            let cfg = self.config.load();
198            if cfg.theme_colors.auto_dark_mode {
199                let is_dark = window
200                    .theme()
201                    .is_none_or(|t| t == winit::window::Theme::Dark);
202                drop(cfg); // release guard before mutation via rcu
203                self.config.rcu(|old| {
204                    let mut new = (**old).clone();
205                    if new.apply_system_theme(is_dark) {
206                        Arc::new(new)
207                    } else {
208                        Arc::clone(old)
209                    }
210                });
211                let cfg = self.config.load();
212                log::info!(
213                    "Auto dark mode: detected {} system theme, using theme: {}",
214                    if is_dark { "dark" } else { "light" },
215                    cfg.theme_colors.theme
216                );
217            }
218        }
219
220        // Detect system theme at startup and apply tab style if tab_style is Automatic
221        {
222            let is_dark = window
223                .theme()
224                .is_none_or(|t| t == winit::window::Theme::Dark);
225            let should_apply = {
226                let mut probe = (**self.config.load()).clone();
227                probe.apply_system_tab_style(is_dark)
228            };
229            if should_apply {
230                self.config.rcu(|old| {
231                    let mut new = (**old).clone();
232                    if new.apply_system_tab_style(is_dark) {
233                        Arc::new(new)
234                    } else {
235                        Arc::clone(old)
236                    }
237                });
238                let cfg = self.config.load();
239                log::info!(
240                    "Auto tab style: detected {} system theme, applying {} tab style",
241                    if is_dark { "dark" } else { "light" },
242                    if is_dark {
243                        cfg.tabs.dark_tab_style.display_name()
244                    } else {
245                        cfg.tabs.light_tab_style.display_name()
246                    }
247                );
248            }
249        }
250
251        let window = Arc::new(window);
252
253        // Initialize egui context and state (no memory to preserve on first init)
254        self.init_egui(&window, false);
255
256        // Create renderer using DRY init params
257        let cfg = self.config.load();
258        let theme = cfg.load_theme();
259        // Get shader metadata from cache for full 3-tier resolution
260        let metadata = cfg
261            .shader
262            .custom_shader
263            .as_ref()
264            .and_then(|name| self.shader_state.shader_metadata_cache.get(name).cloned());
265        // Get cursor shader metadata from cache for full 3-tier resolution
266        let cursor_metadata = cfg.shader.cursor_shader.as_ref().and_then(|name| {
267            self.shader_state
268                .cursor_shader_metadata_cache
269                .get(name)
270                .cloned()
271        });
272        let params = RendererInitParams::from_config(
273            &cfg,
274            &theme,
275            metadata.as_ref(),
276            cursor_metadata.as_ref(),
277        );
278        drop(cfg); // release guard before moving to macOS section
279
280        let mut renderer = params.create_renderer(Arc::clone(&window)).await?;
281        self.collect_startup_shader_errors(&mut renderer);
282
283        // macOS: Configure CAMetalLayer (transparency + performance)
284        // This MUST be done AFTER creating the wgpu surface/renderer
285        // so that the CAMetalLayer has been created by wgpu
286        #[cfg(target_os = "macos")]
287        {
288            if let Err(e) = crate::macos_metal::configure_metal_layer_for_performance(&window) {
289                log::warn!("Failed to configure Metal layer: {}", e);
290                log::warn!(
291                    "Continuing anyway - may experience reduced FPS or missing transparency on macOS"
292                );
293            }
294            // Set initial layer opacity to match config (content only, frame unaffected)
295            if let Err(e) = crate::macos_metal::set_layer_opacity(&window, 1.0) {
296                log::warn!("Failed to set initial Metal layer opacity: {}", e);
297            }
298            // Apply initial blur settings if enabled
299            {
300                let cfg = self.config.load();
301                if cfg.window.blur_enabled
302                    && cfg.window.window_opacity < 1.0
303                    && let Err(e) =
304                        crate::macos_blur::set_window_blur(&window, cfg.window.blur_radius)
305                {
306                    log::warn!("Failed to set initial window blur: {}", e);
307                }
308                // macOS 27 draws the titlebar from the NSWindow background color
309                // when the window is non-opaque; winit's transparent(true) set it
310                // to clear at creation. Restore a real background color unless
311                // per-pixel translucency is active.
312                let translucent = cfg.window.window_opacity < 1.0
313                    || (cfg.background.background_image_enabled
314                        && cfg.background.background_image_opacity < 1.0);
315                if let Err(e) =
316                    crate::macos_metal::set_window_background_for_translucency(&window, translucent)
317                {
318                    log::warn!("Failed to set initial window background: {}", e);
319                }
320            }
321        }
322
323        // Apply cursor shader configuration
324        self.apply_cursor_shader_config(&mut renderer, &params);
325
326        // Set tab bar offsets BEFORE creating the first tab
327        // This ensures the terminal is sized correctly from the start
328        // Use 1 as tab count since we're about to create the first tab
329        let (initial_tab_bar_height, initial_tab_bar_width, tab_bar_mode, tab_bar_position) = {
330            let cfg = self.config.load();
331            (
332                self.tab_bar_ui.get_height(1, &cfg),
333                self.tab_bar_ui.get_width(1, &cfg),
334                cfg.tabs.tab_bar_mode,
335                cfg.tabs.tab_bar_position,
336            )
337        };
338        let (initial_cols, initial_rows) = renderer.grid_size();
339        log::info!(
340            "Tab bar init: mode={:?}, position={:?}, height={:.1}, width={:.1}, initial_grid={}x{}, content_offset_y_before={:.1}",
341            tab_bar_mode,
342            tab_bar_position,
343            initial_tab_bar_height,
344            initial_tab_bar_width,
345            initial_cols,
346            initial_rows,
347            renderer.content_offset_y()
348        );
349        self.apply_tab_bar_offsets(&mut renderer, initial_tab_bar_height, initial_tab_bar_width);
350
351        // Get the renderer's grid size BEFORE storing it (and before creating tabs)
352        // This ensures the shell starts with correct dimensions that account for tab bar
353        let (renderer_cols, renderer_rows) = renderer.grid_size();
354        let cell_width = renderer.cell_width();
355        let cell_height = renderer.cell_height();
356
357        self.window = Some(Arc::clone(&window));
358        self.renderer = Some(renderer);
359
360        // Initialize shader watcher if hot reload is enabled
361        self.init_shader_watcher();
362
363        // Initialize config file watcher for automatic reload
364        self.init_config_watcher();
365
366        // Initialize config-update file watcher (MCP server writes here)
367        self.init_config_update_watcher();
368
369        // Initialize screenshot-request watcher (MCP server screenshot tool writes here)
370        self.init_screenshot_request_watcher();
371
372        // Initialize shader-diagnostics-request watcher (MCP server diagnostics tool writes here)
373        self.init_shader_diagnostics_request_watcher();
374
375        // Sync status bar monitor state based on config
376        {
377            let cfg = self.config.load();
378            self.status_bar_ui.sync_monitor_state(&cfg);
379        }
380
381        // Create the first tab with the correct grid size from the renderer
382        // This ensures the shell is spawned with dimensions that account for tab bar.
383        // Skipped when `skip_default_tab` is true (e.g. "Move Tab to New Window" path).
384        if !skip_default_tab {
385            log::info!(
386                "Creating first tab with grid size {}x{} (accounting for tab bar)",
387                renderer_cols,
388                renderer_rows
389            );
390            let (max_fps, inactive_tab_fps) = {
391                let cfg = self.config.load();
392                (cfg.rendering.max_fps, cfg.power.inactive_tab_fps)
393            };
394            let tab_id = self.tab_manager.new_tab_with_cwd(
395                &self.config.load(),
396                Arc::clone(&self.runtime),
397                first_tab_cwd,
398                Some((renderer_cols, renderer_rows)), // Pass correct grid size
399            )?;
400
401            // Set cell dimensions on the terminal (for TIOCGWINSZ pixel size reporting)
402            if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
403                let width_px = (renderer_cols as f32 * cell_width) as usize;
404                let height_px = (renderer_rows as f32 * cell_height) as usize;
405
406                if let Ok(mut term) = tab.terminal.try_write() {
407                    term.set_cell_dimensions(cell_width as u32, cell_height as u32);
408                    // Send resize to ensure PTY has correct pixel dimensions
409                    if let Err(e) =
410                        term.resize_with_pixels(renderer_cols, renderer_rows, width_px, height_px)
411                    {
412                        crate::debug_error!("TERMINAL", "resize_with_pixels failed (init): {e}");
413                    }
414                    log::info!(
415                        "Initial terminal dimensions: {}x{} ({}x{} px)",
416                        renderer_cols,
417                        renderer_rows,
418                        width_px,
419                        height_px
420                    );
421                }
422
423                // Start refresh task for the first tab
424                tab.start_refresh_task(
425                    Arc::clone(&self.runtime),
426                    Arc::clone(&window),
427                    max_fps,
428                    inactive_tab_fps,
429                );
430            }
431        }
432
433        // Auto-connect agent if panel is open on startup and auto-launch is enabled
434        if self.overlay_ui.ai_inspector.open {
435            self.try_auto_connect_agent();
436        }
437
438        // Check if we should prompt user to install integrations (shaders and/or shell integration)
439        {
440            let cfg = self.config.load();
441            if cfg.should_prompt_integrations(crate::VERSION) {
442                drop(cfg);
443                log::info!("Integrations not installed - showing welcome dialog");
444                self.overlay_ui.integrations_ui.show_dialog();
445                self.focus_state.needs_redraw = true;
446                window.request_redraw();
447            }
448        }
449
450        Ok(())
451    }
452}