par_term/app/window_state/
impl_init.rs1use 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 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 input_handler.update_option_key_modes(
54 config.input.left_option_key_mode,
55 config.input.right_option_key_mode,
56 );
57
58 let badge_state = BadgeState::new(&config);
60 let overlay_ui = crate::app::window_state::overlay_ui_state::OverlayUiState::new(&config);
61
62 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, 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 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 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 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 window.set_ime_allowed(true);
193 log::debug!("IME enabled for character input");
194
195 {
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); 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 {
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 self.init_egui(&window, false);
255
256 let cfg = self.config.load();
258 let theme = cfg.load_theme();
259 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 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); let mut renderer = params.create_renderer(Arc::clone(&window)).await?;
281 self.collect_startup_shader_errors(&mut renderer);
282
283 #[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 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 {
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 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 self.apply_cursor_shader_config(&mut renderer, ¶ms);
325
326 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 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 self.init_shader_watcher();
362
363 self.init_config_watcher();
365
366 self.init_config_update_watcher();
368
369 self.init_screenshot_request_watcher();
371
372 self.init_shader_diagnostics_request_watcher();
374
375 {
377 let cfg = self.config.load();
378 self.status_bar_ui.sync_monitor_state(&cfg);
379 }
380
381 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)), )?;
400
401 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 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 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 if self.overlay_ui.ai_inspector.open {
435 self.try_auto_connect_agent();
436 }
437
438 {
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}