1use crate::app::window_state::WindowState;
7use crate::arrangements::{self, ArrangementId, ArrangementManager};
8use crate::cli::RuntimeOptions;
9use crate::config::{Config, resolve_shader_config};
10use crate::menu::{MenuAction, MenuManager};
11use crate::settings_window::{SettingsWindow, SettingsWindowAction};
12use crate::update_checker::{UpdateCheckResult, UpdateChecker};
13
14fn to_settings_update_result(result: &UpdateCheckResult) -> crate::settings_ui::UpdateCheckResult {
16 match result {
17 UpdateCheckResult::UpToDate => crate::settings_ui::UpdateCheckResult::UpToDate,
18 UpdateCheckResult::UpdateAvailable(info) => {
19 crate::settings_ui::UpdateCheckResult::UpdateAvailable(
20 crate::settings_ui::UpdateCheckInfo {
21 version: info.version.clone(),
22 release_notes: info.release_notes.clone(),
23 release_url: info.release_url.clone(),
24 published_at: info.published_at.clone(),
25 },
26 )
27 }
28 UpdateCheckResult::Disabled => crate::settings_ui::UpdateCheckResult::Disabled,
29 UpdateCheckResult::Skipped => crate::settings_ui::UpdateCheckResult::Skipped,
30 UpdateCheckResult::Error(e) => crate::settings_ui::UpdateCheckResult::Error(e.clone()),
31 }
32}
33use std::collections::HashMap;
34use std::path::PathBuf;
35use std::sync::Arc;
36use std::time::Instant;
37use tokio::runtime::Runtime;
38use winit::event::WindowEvent;
39use winit::event_loop::ActiveEventLoop;
40use winit::window::WindowId;
41
42pub struct WindowManager {
44 pub(crate) windows: HashMap<WindowId, WindowState>,
46 pub(crate) menu: Option<MenuManager>,
48 pub(crate) config: Config,
50 pub(crate) runtime: Arc<Runtime>,
52 pub(crate) should_exit: bool,
54 pending_window_count: usize,
56 pub(crate) settings_window: Option<SettingsWindow>,
58 pub(crate) runtime_options: RuntimeOptions,
60 pub(crate) start_time: Option<Instant>,
62 pub(crate) command_sent: bool,
64 pub(crate) screenshot_taken: bool,
66 pub(crate) update_checker: UpdateChecker,
68 pub(crate) next_update_check: Option<Instant>,
70 pub(crate) last_update_result: Option<UpdateCheckResult>,
72 pub(crate) arrangement_manager: ArrangementManager,
74 pub(crate) auto_restore_done: bool,
76 pub(crate) dynamic_profile_manager: crate::profile::DynamicProfileManager,
78}
79
80impl WindowManager {
81 pub fn new(config: Config, runtime: Arc<Runtime>, runtime_options: RuntimeOptions) -> Self {
83 let arrangement_manager = match arrangements::storage::load_arrangements() {
85 Ok(manager) => manager,
86 Err(e) => {
87 log::warn!("Failed to load arrangements: {}", e);
88 ArrangementManager::new()
89 }
90 };
91
92 let mut dynamic_profile_manager = crate::profile::DynamicProfileManager::new();
93 if !config.dynamic_profile_sources.is_empty() {
94 dynamic_profile_manager.start(&config.dynamic_profile_sources, &runtime);
95 }
96
97 Self {
98 windows: HashMap::new(),
99 menu: None,
100 config,
101 runtime,
102 should_exit: false,
103 pending_window_count: 0,
104 settings_window: None,
105 runtime_options,
106 start_time: None,
107 command_sent: false,
108 screenshot_taken: false,
109 update_checker: UpdateChecker::new(env!("CARGO_PKG_VERSION")),
110 next_update_check: None,
111 last_update_result: None,
112 arrangement_manager,
113 auto_restore_done: false,
114 dynamic_profile_manager,
115 }
116 }
117
118 pub fn get_focused_window_id(&self) -> Option<WindowId> {
121 for (window_id, window_state) in &self.windows {
123 if window_state.is_focused {
124 return Some(*window_id);
125 }
126 }
127 self.windows.keys().next().copied()
130 }
131
132 pub fn check_cli_timers(&mut self) {
134 let Some(start_time) = self.start_time else {
135 return;
136 };
137
138 let elapsed = start_time.elapsed().as_secs_f64();
139
140 if !self.command_sent
142 && elapsed >= 1.0
143 && let Some(cmd) = self.runtime_options.command_to_send.clone()
144 {
145 self.send_command_to_shell(&cmd);
146 self.command_sent = true;
147 }
148
149 if !self.screenshot_taken && self.runtime_options.screenshot.is_some() {
151 let screenshot_time = self
152 .runtime_options
153 .exit_after
154 .map(|t| t - 1.0)
155 .unwrap_or(2.0);
156 if elapsed >= screenshot_time {
157 self.take_screenshot();
158 self.screenshot_taken = true;
159 }
160 }
161
162 if let Some(exit_after) = self.runtime_options.exit_after
164 && elapsed >= exit_after
165 {
166 log::info!("Exit-after timer expired ({:.1}s), exiting", exit_after);
167 self.should_exit = true;
168 }
169 }
170
171 pub fn check_for_updates(&mut self) {
173 use crate::update_checker::current_timestamp;
174 use std::time::Duration;
175
176 let now = Instant::now();
177
178 if self.next_update_check.is_none() {
180 self.next_update_check = Some(now + Duration::from_secs(5));
181 return;
182 }
183
184 if let Some(next_check) = self.next_update_check
186 && now >= next_check
187 {
188 let (result, should_save) = self.update_checker.check_now(&self.config, false);
190
191 let mut config_changed = should_save;
193 match &result {
194 UpdateCheckResult::UpdateAvailable(info) => {
195 let version_str = info
196 .version
197 .strip_prefix('v')
198 .unwrap_or(&info.version)
199 .to_string();
200
201 log::info!(
202 "Update available: {} (current: {})",
203 version_str,
204 env!("CARGO_PKG_VERSION")
205 );
206
207 let already_notified = self
209 .config
210 .last_notified_version
211 .as_ref()
212 .is_some_and(|v| v == &version_str);
213
214 if !already_notified {
215 self.notify_update_available(info);
216 self.config.last_notified_version = Some(version_str);
217 config_changed = true;
218 }
219 }
220 UpdateCheckResult::UpToDate => {
221 log::info!("par-term is up to date ({})", env!("CARGO_PKG_VERSION"));
222 }
223 UpdateCheckResult::Error(e) => {
224 log::warn!("Update check failed: {}", e);
225 }
226 UpdateCheckResult::Disabled | UpdateCheckResult::Skipped => {
227 }
229 }
230
231 self.last_update_result = Some(result);
232
233 if config_changed {
235 self.config.last_update_check = Some(current_timestamp());
236 if let Err(e) = self.config.save() {
237 log::warn!("Failed to save config after update check: {}", e);
238 }
239 }
240
241 self.next_update_check = self
243 .config
244 .update_check_frequency
245 .as_seconds()
246 .map(|secs| now + Duration::from_secs(secs));
247 }
248 }
249
250 fn notify_update_available(&self, info: &crate::update_checker::UpdateInfo) {
252 let version_str = info.version.strip_prefix('v').unwrap_or(&info.version);
253 let current = env!("CARGO_PKG_VERSION");
254 let summary = format!("par-term v{} Available", version_str);
255 let body = format!(
256 "You have v{}. Check Settings > Advanced > Updates.",
257 current
258 );
259
260 #[cfg(not(target_os = "macos"))]
261 {
262 use notify_rust::Notification;
263 let _ = Notification::new()
264 .summary(&summary)
265 .body(&body)
266 .appname("par-term")
267 .timeout(notify_rust::Timeout::Milliseconds(8000))
268 .show();
269 }
270
271 #[cfg(target_os = "macos")]
272 {
273 let script = format!(
274 r#"display notification "{}" with title "{}""#,
275 body.replace('"', r#"\""#),
276 summary.replace('"', r#"\""#),
277 );
278 let _ = std::process::Command::new("osascript")
279 .arg("-e")
280 .arg(&script)
281 .spawn();
282 }
283 }
284
285 pub fn force_update_check(&mut self) {
287 use crate::update_checker::current_timestamp;
288
289 let (result, should_save) = self.update_checker.check_now(&self.config, true);
290
291 match &result {
293 UpdateCheckResult::UpdateAvailable(info) => {
294 log::info!(
295 "Update available: {} (current: {})",
296 info.version,
297 env!("CARGO_PKG_VERSION")
298 );
299 }
300 UpdateCheckResult::UpToDate => {
301 log::info!("par-term is up to date ({})", env!("CARGO_PKG_VERSION"));
302 }
303 UpdateCheckResult::Error(e) => {
304 log::warn!("Update check failed: {}", e);
305 }
306 _ => {}
307 }
308
309 self.last_update_result = Some(result);
310
311 if should_save {
313 self.config.last_update_check = Some(current_timestamp());
314 if let Err(e) = self.config.save() {
315 log::warn!("Failed to save config after update check: {}", e);
316 }
317 }
318 }
319
320 pub fn force_update_check_for_settings(&mut self) {
322 self.force_update_check();
323 if let Some(settings_window) = &mut self.settings_window {
325 settings_window.settings_ui.last_update_result = self
326 .last_update_result
327 .as_ref()
328 .map(to_settings_update_result);
329 settings_window.request_redraw();
330 }
331 }
332
333 fn send_command_to_shell(&mut self, cmd: &str) {
335 if let Some(window_state) = self.windows.values_mut().next()
337 && let Some(tab) = window_state.tab_manager.active_tab_mut()
338 {
339 let cmd_with_enter = format!("{}\n", cmd);
341 if let Ok(term) = tab.terminal.try_lock() {
342 if let Err(e) = term.write(cmd_with_enter.as_bytes()) {
343 log::error!("Failed to send command to shell: {}", e);
344 } else {
345 log::info!("Sent command to shell: {}", cmd);
346 }
347 }
348 }
349 }
350
351 fn take_screenshot(&mut self) {
353 log::info!("Taking screenshot...");
354
355 let output_path = match &self.runtime_options.screenshot {
357 Some(path) if !path.as_os_str().is_empty() => {
358 log::info!("Screenshot path specified: {:?}", path);
359 path.clone()
360 }
361 _ => {
362 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
364 let path = PathBuf::from(format!("par-term-{}.png", timestamp));
365 log::info!("Using auto-generated screenshot path: {:?}", path);
366 path
367 }
368 };
369
370 if let Some(window_state) = self.windows.values_mut().next() {
372 if let Some(renderer) = &mut window_state.renderer {
373 log::info!("Capturing screenshot from renderer...");
374 match renderer.take_screenshot() {
375 Ok(image_data) => {
376 log::info!(
377 "Screenshot captured: {}x{} pixels",
378 image_data.width(),
379 image_data.height()
380 );
381 if let Err(e) = image_data.save(&output_path) {
383 log::error!("Failed to save screenshot to {:?}: {}", output_path, e);
384 } else {
385 log::info!("Screenshot saved to {:?}", output_path);
386 }
387 }
388 Err(e) => {
389 log::error!("Failed to take screenshot: {}", e);
390 }
391 }
392 } else {
393 log::warn!("No renderer available for screenshot");
394 }
395 } else {
396 log::warn!("No window available for screenshot");
397 }
398 }
399
400 pub fn create_window(&mut self, event_loop: &ActiveEventLoop) {
402 use crate::config::WindowType;
403 use crate::font_metrics::window_size_from_config;
404 use winit::window::Window;
405
406 if let Ok(fresh_config) = Config::load() {
410 self.config = fresh_config;
411 }
412
413 let (width, height) = window_size_from_config(&self.config, 1.0).unwrap_or((800, 600));
419
420 let window_number = self.windows.len() + 1;
422 let title = if self.config.show_window_number {
423 format!("{} [{}]", self.config.window_title, window_number)
424 } else {
425 self.config.window_title.clone()
426 };
427
428 let mut window_attrs = Window::default_attributes()
429 .with_title(&title)
430 .with_inner_size(winit::dpi::LogicalSize::new(width, height))
431 .with_decorations(self.config.window_decorations);
432
433 if self.config.lock_window_size {
435 window_attrs = window_attrs.with_resizable(false);
436 log::info!("Window size locked (resizing disabled)");
437 }
438
439 if self.config.window_type == WindowType::Fullscreen {
441 window_attrs =
442 window_attrs.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
443 log::info!("Window starting in fullscreen mode");
444 }
445
446 let icon_bytes = include_bytes!("../../assets/icon.png");
448 if let Ok(icon_image) = image::load_from_memory(icon_bytes) {
449 let rgba = icon_image.to_rgba8();
450 let (width, height) = rgba.dimensions();
451 if let Ok(icon) = winit::window::Icon::from_rgba(rgba.into_raw(), width, height) {
452 window_attrs = window_attrs.with_window_icon(Some(icon));
453 log::info!("Window icon set ({}x{})", width, height);
454 } else {
455 log::warn!("Failed to create window icon from RGBA data");
456 }
457 } else {
458 log::warn!("Failed to load embedded icon image");
459 }
460
461 if self.config.window_always_on_top {
463 window_attrs = window_attrs.with_window_level(winit::window::WindowLevel::AlwaysOnTop);
464 log::info!("Window always-on-top enabled");
465 }
466
467 window_attrs = window_attrs.with_transparent(true);
469 log::info!(
470 "Window transparency enabled (opacity: {})",
471 self.config.window_opacity
472 );
473
474 match event_loop.create_window(window_attrs) {
475 Ok(window) => {
476 let window_id = window.id();
477 let mut window_state =
478 WindowState::new(self.config.clone(), Arc::clone(&self.runtime));
479 window_state.window_index = window_number;
481
482 let runtime = Arc::clone(&self.runtime);
484 if let Err(e) = runtime.block_on(window_state.initialize_async(window)) {
485 log::error!("Failed to initialize window: {}", e);
486 return;
487 }
488
489 if self.menu.is_none() {
491 match MenuManager::new() {
492 Ok(menu) => {
493 if let Some(win) = &window_state.window
495 && let Err(e) = menu.init_for_window(win)
496 {
497 log::warn!("Failed to initialize menu for window: {}", e);
498 }
499 self.menu = Some(menu);
500 }
501 Err(e) => {
502 log::warn!("Failed to create menu: {}", e);
503 }
504 }
505 } else if let Some(menu) = &self.menu
506 && let Some(win) = &window_state.window
507 && let Err(e) = menu.init_for_window(win)
508 {
509 log::warn!("Failed to initialize menu for window: {}", e);
511 }
512
513 if let Some(win) = &window_state.window {
515 self.apply_window_positioning(win, event_loop);
516 }
517
518 if self.windows.is_empty()
520 && window_state.config.tmux_enabled
521 && window_state.config.tmux_auto_attach
522 {
523 let session_name = window_state.config.tmux_auto_attach_session.clone();
524
525 if let Some(ref name) = session_name {
527 if !name.is_empty() {
528 log::info!(
529 "tmux auto-attach: attempting to attach to session '{}' via gateway",
530 name
531 );
532 match window_state.attach_tmux_gateway(name) {
533 Ok(()) => {
534 log::info!(
535 "tmux auto-attach: gateway initiated for session '{}'",
536 name
537 );
538 }
539 Err(e) => {
540 log::warn!(
541 "tmux auto-attach: failed to attach to '{}': {} - continuing without tmux",
542 name,
543 e
544 );
545 }
547 }
548 } else {
549 log::info!(
551 "tmux auto-attach: no session specified, creating new session via gateway"
552 );
553 if let Err(e) = window_state.initiate_tmux_gateway(None) {
554 log::warn!(
555 "tmux auto-attach: failed to create new session: {} - continuing without tmux",
556 e
557 );
558 }
559 }
560 } else {
561 log::info!(
563 "tmux auto-attach: no session specified, creating new session via gateway"
564 );
565 if let Err(e) = window_state.initiate_tmux_gateway(None) {
566 log::warn!(
567 "tmux auto-attach: failed to create new session: {} - continuing without tmux",
568 e
569 );
570 }
571 }
572 }
573
574 self.windows.insert(window_id, window_state);
575 self.pending_window_count += 1;
576
577 if self.start_time.is_none() {
579 self.start_time = Some(Instant::now());
580 }
581
582 log::info!(
583 "Created new window {:?} (total: {})",
584 window_id,
585 self.windows.len()
586 );
587 }
588 Err(e) => {
589 log::error!("Failed to create window: {}", e);
590 }
591 }
592 }
593
594 fn apply_window_positioning(
596 &self,
597 window: &std::sync::Arc<winit::window::Window>,
598 event_loop: &ActiveEventLoop,
599 ) {
600 use crate::config::WindowType;
601
602 let monitors: Vec<_> = event_loop.available_monitors().collect();
604 if monitors.is_empty() {
605 log::warn!("No monitors available for window positioning");
606 return;
607 }
608
609 let monitor = if let Some(index) = self.config.target_monitor {
611 monitors
612 .get(index)
613 .cloned()
614 .or_else(|| monitors.first().cloned())
615 } else {
616 event_loop
617 .primary_monitor()
618 .or_else(|| monitors.first().cloned())
619 };
620
621 let Some(monitor) = monitor else {
622 log::warn!("Could not determine target monitor");
623 return;
624 };
625
626 let monitor_pos = monitor.position();
627 let monitor_size = monitor.size();
628 let window_size = window.outer_size();
629
630 match self.config.window_type {
632 WindowType::EdgeTop => {
633 window.set_outer_position(winit::dpi::PhysicalPosition::new(
635 monitor_pos.x,
636 monitor_pos.y,
637 ));
638 let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(
639 monitor_size.width,
640 window_size.height,
641 ));
642 log::info!("Window positioned at top edge of monitor");
643 }
644 WindowType::EdgeBottom => {
645 let y = monitor_pos.y + monitor_size.height as i32 - window_size.height as i32;
647 window.set_outer_position(winit::dpi::PhysicalPosition::new(monitor_pos.x, y));
648 let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(
649 monitor_size.width,
650 window_size.height,
651 ));
652 log::info!("Window positioned at bottom edge of monitor");
653 }
654 WindowType::EdgeLeft => {
655 window.set_outer_position(winit::dpi::PhysicalPosition::new(
657 monitor_pos.x,
658 monitor_pos.y,
659 ));
660 let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(
661 window_size.width,
662 monitor_size.height,
663 ));
664 log::info!("Window positioned at left edge of monitor");
665 }
666 WindowType::EdgeRight => {
667 let x = monitor_pos.x + monitor_size.width as i32 - window_size.width as i32;
669 window.set_outer_position(winit::dpi::PhysicalPosition::new(x, monitor_pos.y));
670 let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(
671 window_size.width,
672 monitor_size.height,
673 ));
674 log::info!("Window positioned at right edge of monitor");
675 }
676 WindowType::Normal | WindowType::Fullscreen => {
677 if self.config.target_monitor.is_some() {
679 let x =
681 monitor_pos.x + (monitor_size.width as i32 - window_size.width as i32) / 2;
682 let y = monitor_pos.y
683 + (monitor_size.height as i32 - window_size.height as i32) / 2;
684 window.set_outer_position(winit::dpi::PhysicalPosition::new(x, y));
685 log::info!(
686 "Window centered on monitor {} at ({}, {})",
687 self.config.target_monitor.unwrap_or(0),
688 x,
689 y
690 );
691 }
692 }
693 }
694
695 if let Some(space) = self.config.target_space
697 && let Err(e) = crate::macos_space::move_window_to_space(window, space)
698 {
699 log::warn!("Failed to move window to Space {}: {}", space, e);
700 }
701 }
702
703 pub fn close_window(&mut self, window_id: WindowId) {
705 if self.config.restore_session
708 && self.windows.len() == 1
709 && self.windows.contains_key(&window_id)
710 {
711 self.save_session_state_background();
712 }
713
714 if let Some(window_state) = self.windows.remove(&window_id) {
715 log::info!(
716 "Closing window {:?} (remaining: {})",
717 window_id,
718 self.windows.len()
719 );
720 if let Some(ref window) = window_state.window {
722 window.set_visible(false);
723 }
724 drop(window_state);
726 }
727
728 if self.windows.is_empty() {
730 log::info!("Last window closed, exiting application");
731 if self.settings_window.is_some() {
734 log::info!("Closing settings window before exit");
735 self.close_settings_window();
736 }
737 self.should_exit = true;
738 }
739 }
740
741 fn save_session_state_background(&self) {
744 let state = crate::session::capture::capture_session(&self.windows);
745 let _ = std::thread::Builder::new()
746 .name("session-save".into())
747 .spawn(move || {
748 if let Err(e) = crate::session::storage::save_session(&state) {
749 log::error!("Failed to save session state: {}", e);
750 }
751 });
752 }
753
754 pub fn restore_session(&mut self, event_loop: &ActiveEventLoop) -> bool {
758 let session = match crate::session::storage::load_session() {
759 Ok(Some(session)) => session,
760 Ok(None) => {
761 log::info!("No saved session found, creating default window");
762 return false;
763 }
764 Err(e) => {
765 log::warn!(
766 "Failed to load session state: {}, creating default window",
767 e
768 );
769 return false;
770 }
771 };
772
773 if session.windows.is_empty() {
774 log::info!("Saved session has no windows, creating default window");
775 return false;
776 }
777
778 log::info!(
779 "Restoring session ({} windows) saved at {}",
780 session.windows.len(),
781 session.saved_at
782 );
783
784 for session_window in &session.windows {
785 let tab_cwds: Vec<Option<String>> = session_window
787 .tabs
788 .iter()
789 .map(|tab| crate::session::restore::validate_cwd(&tab.cwd))
790 .collect();
791
792 self.create_window_with_overrides(
793 event_loop,
794 session_window.position,
795 session_window.size,
796 &tab_cwds,
797 session_window.active_tab_index,
798 );
799
800 if let Some((_window_id, window_state)) = self.windows.iter_mut().last() {
803 let tabs = window_state.tab_manager.tabs_mut();
804 for (tab_idx, session_tab) in session_window.tabs.iter().enumerate() {
805 if let Some(ref layout) = session_tab.pane_layout
806 && let Some(tab) = tabs.get_mut(tab_idx)
807 {
808 tab.restore_pane_layout(layout, &self.config, Arc::clone(&self.runtime));
809 }
810 }
811 }
812 }
813
814 if let Err(e) = crate::session::storage::clear_session() {
816 log::warn!("Failed to clear session file after restore: {}", e);
817 }
818
819 if self.windows.is_empty() {
821 log::warn!("Session restore created no windows, creating default");
822 return false;
823 }
824
825 true
826 }
827
828 #[allow(dead_code)]
830 pub fn get_window_mut(&mut self, window_id: WindowId) -> Option<&mut WindowState> {
831 self.windows.get_mut(&window_id)
832 }
833
834 #[allow(dead_code)]
836 pub fn get_window(&self, window_id: WindowId) -> Option<&WindowState> {
837 self.windows.get(&window_id)
838 }
839
840 pub fn handle_menu_action(
842 &mut self,
843 action: MenuAction,
844 event_loop: &ActiveEventLoop,
845 focused_window: Option<WindowId>,
846 ) {
847 match action {
848 MenuAction::NewWindow => {
849 self.create_window(event_loop);
850 }
851 MenuAction::CloseWindow => {
852 if let Some(window_id) = focused_window
854 && let Some(window_state) = self.windows.get_mut(&window_id)
855 && window_state.close_current_tab()
856 {
857 self.close_window(window_id);
859 }
860 }
861 MenuAction::NewTab => {
862 if let Some(window_id) = focused_window
863 && let Some(window_state) = self.windows.get_mut(&window_id)
864 {
865 window_state.new_tab();
866 }
867 }
868 MenuAction::CloseTab => {
869 if let Some(window_id) = focused_window
870 && let Some(window_state) = self.windows.get_mut(&window_id)
871 && window_state.close_current_tab()
872 {
873 self.close_window(window_id);
875 }
876 }
877 MenuAction::NextTab => {
878 if let Some(window_id) = focused_window
879 && let Some(window_state) = self.windows.get_mut(&window_id)
880 {
881 window_state.next_tab();
882 }
883 }
884 MenuAction::PreviousTab => {
885 if let Some(window_id) = focused_window
886 && let Some(window_state) = self.windows.get_mut(&window_id)
887 {
888 window_state.prev_tab();
889 }
890 }
891 MenuAction::SwitchToTab(index) => {
892 if let Some(window_id) = focused_window
893 && let Some(window_state) = self.windows.get_mut(&window_id)
894 {
895 window_state.switch_to_tab_index(index);
896 }
897 }
898 MenuAction::MoveTabLeft => {
899 if let Some(window_id) = focused_window
900 && let Some(window_state) = self.windows.get_mut(&window_id)
901 {
902 window_state.move_tab_left();
903 }
904 }
905 MenuAction::MoveTabRight => {
906 if let Some(window_id) = focused_window
907 && let Some(window_state) = self.windows.get_mut(&window_id)
908 {
909 window_state.move_tab_right();
910 }
911 }
912 MenuAction::DuplicateTab => {
913 if let Some(window_id) = focused_window
914 && let Some(window_state) = self.windows.get_mut(&window_id)
915 {
916 window_state.duplicate_tab();
917 }
918 }
919 MenuAction::Quit => {
920 let window_ids: Vec<_> = self.windows.keys().copied().collect();
922 for window_id in window_ids {
923 self.close_window(window_id);
924 }
925 }
926 MenuAction::Copy => {
927 if let Some(sw) = &self.settings_window
929 && sw.is_focused()
930 {
931 if let Some(sw) = &mut self.settings_window {
932 sw.inject_event(egui::Event::Copy);
933 }
934 return;
935 }
936 if let Some(window_id) = focused_window
938 && let Some(window_state) = self.windows.get_mut(&window_id)
939 && window_state.has_egui_overlay_visible()
940 {
941 window_state.pending_egui_events.push(egui::Event::Copy);
942 return;
943 }
944 if let Some(window_id) = focused_window
945 && let Some(window_state) = self.windows.get_mut(&window_id)
946 && let Some(text) = window_state.get_selected_text()
947 {
948 if let Err(e) = window_state.input_handler.copy_to_clipboard(&text) {
949 log::error!("Failed to copy to clipboard: {}", e);
950 } else {
951 window_state.sync_clipboard_to_tmux(&text);
953 }
954 }
955 }
956 MenuAction::Paste => {
957 if let Some(sw) = &self.settings_window
960 && sw.is_focused()
961 {
962 if let Ok(mut clipboard) = arboard::Clipboard::new() {
963 if let Ok(text) = clipboard.get_text() {
964 if let Some(sw) = &mut self.settings_window {
965 sw.inject_paste(text);
966 }
967 return;
968 }
969 if clipboard.get_image().is_err() {
973 return;
975 }
976 } else {
977 return;
978 }
979 }
980 if let Some(window_id) = focused_window
982 && let Some(window_state) = self.windows.get_mut(&window_id)
983 && window_state.has_egui_overlay_visible()
984 {
985 if let Ok(mut clipboard) = arboard::Clipboard::new() {
986 if let Ok(text) = clipboard.get_text() {
987 window_state
988 .pending_egui_events
989 .push(egui::Event::Paste(text));
990 return;
991 }
992 if clipboard.get_image().is_err() {
995 return;
996 }
997 } else {
998 return;
999 }
1000 }
1001 if let Some(window_id) = focused_window
1002 && let Some(window_state) = self.windows.get_mut(&window_id)
1003 {
1004 if let Some(text) = window_state.input_handler.paste_from_clipboard() {
1005 window_state.paste_text(&text);
1006 } else if window_state.input_handler.clipboard_has_image() {
1007 if let Some(tab) = window_state.tab_manager.active_tab() {
1010 let terminal_clone = Arc::clone(&tab.terminal);
1011 window_state.runtime.spawn(async move {
1012 let term = terminal_clone.lock().await;
1013 let _ = term.write(b"\x16");
1014 });
1015 }
1016 }
1017 }
1018 }
1019 MenuAction::SelectAll => {
1020 if let Some(sw) = &self.settings_window
1022 && sw.is_focused()
1023 {
1024 if let Some(sw) = &mut self.settings_window {
1025 sw.inject_event(egui::Event::Key {
1027 key: egui::Key::A,
1028 physical_key: None,
1029 pressed: true,
1030 repeat: false,
1031 modifiers: egui::Modifiers::COMMAND,
1032 });
1033 }
1034 return;
1035 }
1036 if let Some(window_id) = focused_window
1038 && let Some(window_state) = self.windows.get_mut(&window_id)
1039 && window_state.has_egui_overlay_visible()
1040 {
1041 window_state.pending_egui_events.push(egui::Event::Key {
1042 key: egui::Key::A,
1043 physical_key: None,
1044 pressed: true,
1045 repeat: false,
1046 modifiers: egui::Modifiers::COMMAND,
1047 });
1048 return;
1049 }
1050 log::debug!("SelectAll menu action (not implemented for terminal)");
1052 }
1053 MenuAction::ClearScrollback => {
1054 if let Some(window_id) = focused_window
1055 && let Some(window_state) = self.windows.get_mut(&window_id)
1056 {
1057 let cleared = if let Some(tab) = window_state.tab_manager.active_tab_mut() {
1059 if let Ok(mut term) = tab.terminal.try_lock() {
1060 term.clear_scrollback();
1061 term.clear_scrollback_metadata();
1062 tab.cache.scrollback_len = 0;
1063 tab.trigger_marks.clear();
1064 true
1065 } else {
1066 false
1067 }
1068 } else {
1069 false
1070 };
1071
1072 if cleared {
1073 window_state.set_scroll_target(0);
1074 log::info!("Cleared scrollback buffer");
1075 }
1076 }
1077 }
1078 MenuAction::ClipboardHistory => {
1079 if let Some(window_id) = focused_window
1080 && let Some(window_state) = self.windows.get_mut(&window_id)
1081 {
1082 window_state.clipboard_history_ui.toggle();
1083 window_state.needs_redraw = true;
1084 }
1085 }
1086 MenuAction::ToggleFullscreen => {
1087 if let Some(window_id) = focused_window
1088 && let Some(window_state) = self.windows.get_mut(&window_id)
1089 && let Some(window) = &window_state.window
1090 {
1091 window_state.is_fullscreen = !window_state.is_fullscreen;
1092 if window_state.is_fullscreen {
1093 window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
1094 } else {
1095 window.set_fullscreen(None);
1096 }
1097 }
1098 }
1099 MenuAction::MaximizeVertically => {
1100 if let Some(window_id) = focused_window
1101 && let Some(window_state) = self.windows.get_mut(&window_id)
1102 && let Some(window) = &window_state.window
1103 {
1104 if let Some(monitor) = window.current_monitor() {
1106 let monitor_pos = monitor.position();
1107 let monitor_size = monitor.size();
1108 let window_pos = window.outer_position().unwrap_or_default();
1109 let window_size = window.outer_size();
1110
1111 window.set_outer_position(winit::dpi::PhysicalPosition::new(
1113 window_pos.x,
1114 monitor_pos.y,
1115 ));
1116 let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(
1117 window_size.width,
1118 monitor_size.height,
1119 ));
1120 log::info!(
1121 "Window maximized vertically to {} pixels",
1122 monitor_size.height
1123 );
1124 }
1125 }
1126 }
1127 MenuAction::IncreaseFontSize => {
1128 if let Some(window_id) = focused_window
1129 && let Some(window_state) = self.windows.get_mut(&window_id)
1130 {
1131 window_state.config.font_size = (window_state.config.font_size + 1.0).min(72.0);
1132 window_state.pending_font_rebuild = true;
1133 if let Some(window) = &window_state.window {
1134 window.request_redraw();
1135 }
1136 }
1137 }
1138 MenuAction::DecreaseFontSize => {
1139 if let Some(window_id) = focused_window
1140 && let Some(window_state) = self.windows.get_mut(&window_id)
1141 {
1142 window_state.config.font_size = (window_state.config.font_size - 1.0).max(6.0);
1143 window_state.pending_font_rebuild = true;
1144 if let Some(window) = &window_state.window {
1145 window.request_redraw();
1146 }
1147 }
1148 }
1149 MenuAction::ResetFontSize => {
1150 if let Some(window_id) = focused_window
1151 && let Some(window_state) = self.windows.get_mut(&window_id)
1152 {
1153 window_state.config.font_size = 14.0;
1154 window_state.pending_font_rebuild = true;
1155 if let Some(window) = &window_state.window {
1156 window.request_redraw();
1157 }
1158 }
1159 }
1160 MenuAction::ToggleFpsOverlay => {
1161 if let Some(window_id) = focused_window
1162 && let Some(window_state) = self.windows.get_mut(&window_id)
1163 {
1164 window_state.debug.show_fps_overlay = !window_state.debug.show_fps_overlay;
1165 if let Some(window) = &window_state.window {
1166 window.request_redraw();
1167 }
1168 }
1169 }
1170 MenuAction::OpenSettings => {
1171 self.open_settings_window(event_loop);
1172 }
1173 MenuAction::Minimize => {
1174 if let Some(window_id) = focused_window
1175 && let Some(window_state) = self.windows.get(&window_id)
1176 && let Some(window) = &window_state.window
1177 {
1178 window.set_minimized(true);
1179 }
1180 }
1181 MenuAction::Zoom => {
1182 if let Some(window_id) = focused_window
1183 && let Some(window_state) = self.windows.get(&window_id)
1184 && let Some(window) = &window_state.window
1185 {
1186 window.set_maximized(!window.is_maximized());
1187 }
1188 }
1189 MenuAction::ShowHelp => {
1190 if let Some(window_id) = focused_window
1191 && let Some(window_state) = self.windows.get_mut(&window_id)
1192 {
1193 window_state.help_ui.toggle();
1194 if let Some(window) = &window_state.window {
1195 window.request_redraw();
1196 }
1197 }
1198 }
1199 MenuAction::About => {
1200 log::info!("About par-term v{}", env!("CARGO_PKG_VERSION"));
1201 }
1203 MenuAction::ToggleBackgroundShader => {
1204 if let Some(window_id) = focused_window
1205 && let Some(window_state) = self.windows.get_mut(&window_id)
1206 {
1207 window_state.toggle_background_shader();
1208 }
1209 }
1210 MenuAction::ToggleCursorShader => {
1211 if let Some(window_id) = focused_window
1212 && let Some(window_state) = self.windows.get_mut(&window_id)
1213 {
1214 window_state.toggle_cursor_shader();
1215 }
1216 }
1217 MenuAction::ReloadConfig => {
1218 if let Some(window_id) = focused_window
1219 && let Some(window_state) = self.windows.get_mut(&window_id)
1220 {
1221 window_state.reload_config();
1222 }
1223 }
1224 MenuAction::ManageProfiles => {
1225 self.open_settings_window(event_loop);
1226 if let Some(sw) = &mut self.settings_window {
1227 sw.settings_ui
1228 .set_selected_tab(crate::settings_ui::sidebar::SettingsTab::Profiles);
1229 }
1230 }
1231 MenuAction::ToggleProfileDrawer => {
1232 if let Some(window_id) = focused_window
1233 && let Some(window_state) = self.windows.get_mut(&window_id)
1234 {
1235 window_state.toggle_profile_drawer();
1236 }
1237 }
1238 MenuAction::OpenProfile(profile_id) => {
1239 if let Some(window_id) = focused_window
1240 && let Some(window_state) = self.windows.get_mut(&window_id)
1241 {
1242 window_state.open_profile(profile_id);
1243 }
1244 }
1245 MenuAction::SaveArrangement => {
1246 self.open_settings_window(event_loop);
1248 if let Some(sw) = &mut self.settings_window {
1249 sw.settings_ui
1250 .set_selected_tab(crate::settings_ui::sidebar::SettingsTab::Arrangements);
1251 }
1252 }
1253 MenuAction::InstallShellIntegrationRemote => {
1254 if let Some(window_id) = focused_window
1255 && let Some(window_state) = self.windows.get_mut(&window_id)
1256 {
1257 window_state.remote_shell_install_ui.show_dialog();
1258 window_state.needs_redraw = true;
1259 }
1260 }
1261 }
1262 }
1263
1264 pub fn process_menu_events(
1266 &mut self,
1267 event_loop: &ActiveEventLoop,
1268 focused_window: Option<WindowId>,
1269 ) {
1270 if let Some(menu) = &self.menu {
1271 let actions: Vec<_> = menu.poll_events().collect();
1273 for action in actions {
1274 self.handle_menu_action(action, event_loop, focused_window);
1275 }
1276 }
1277 }
1278
1279 pub fn open_settings_window(&mut self, event_loop: &ActiveEventLoop) {
1281 if let Some(settings_window) = &self.settings_window {
1283 settings_window.focus();
1284 return;
1285 }
1286
1287 let config = self.config.clone();
1289 let runtime = Arc::clone(&self.runtime);
1290
1291 let supported_vsync_modes: Vec<crate::config::VsyncMode> = self
1293 .windows
1294 .values()
1295 .next()
1296 .and_then(|ws| ws.renderer.as_ref())
1297 .map(|renderer| {
1298 [
1299 crate::config::VsyncMode::Immediate,
1300 crate::config::VsyncMode::Mailbox,
1301 crate::config::VsyncMode::Fifo,
1302 ]
1303 .into_iter()
1304 .filter(|mode| renderer.is_vsync_mode_supported(*mode))
1305 .collect()
1306 })
1307 .unwrap_or_else(|| vec![crate::config::VsyncMode::Fifo]); match runtime.block_on(SettingsWindow::new(
1310 event_loop,
1311 config,
1312 supported_vsync_modes,
1313 )) {
1314 Ok(mut settings_window) => {
1315 log::info!("Opened settings window {:?}", settings_window.window_id());
1316 settings_window.settings_ui.app_version = env!("CARGO_PKG_VERSION");
1318 settings_window
1320 .settings_ui
1321 .shell_integration_detected_shell_fn =
1322 Some(crate::shell_integration_installer::detected_shell);
1323 settings_window
1324 .settings_ui
1325 .shell_integration_is_installed_fn =
1326 Some(crate::shell_integration_installer::is_installed);
1327 settings_window.settings_ui.last_update_result = self
1329 .last_update_result
1330 .as_ref()
1331 .map(to_settings_update_result);
1332 let profiles = self
1334 .windows
1335 .values()
1336 .next()
1337 .map(|ws| ws.profile_manager.to_vec())
1338 .unwrap_or_default();
1339 settings_window.settings_ui.sync_profiles(profiles);
1340 if let Some(ws) = self.windows.values().next() {
1342 settings_window.settings_ui.available_agent_ids = ws
1343 .available_agents
1344 .iter()
1345 .map(|a| (a.identity.clone(), a.name.clone()))
1346 .collect();
1347 }
1348 self.settings_window = Some(settings_window);
1349 self.sync_arrangements_to_settings();
1351 }
1352 Err(e) => {
1353 log::error!("Failed to create settings window: {}", e);
1354 }
1355 }
1356 }
1357
1358 pub fn close_settings_window(&mut self) {
1360 if let Some(settings_window) = self.settings_window.take() {
1361 let collapsed = settings_window.settings_ui.collapsed_sections_snapshot();
1364 if !collapsed.is_empty() || !self.config.collapsed_settings_sections.is_empty() {
1365 match Config::load() {
1366 Ok(mut disk_config) => {
1367 disk_config.collapsed_settings_sections = collapsed.clone();
1368 if let Err(e) = disk_config.save() {
1369 log::error!("Failed to persist settings section states: {}", e);
1370 }
1371 }
1372 Err(e) => {
1373 log::error!("Failed to load config for section state save: {}", e);
1374 }
1375 }
1376 self.config.collapsed_settings_sections = collapsed;
1378 }
1379 log::info!("Closed settings window");
1380 }
1381 }
1382
1383 pub fn is_settings_window(&self, window_id: WindowId) -> bool {
1385 self.settings_window
1386 .as_ref()
1387 .is_some_and(|sw| sw.window_id() == window_id)
1388 }
1389
1390 pub fn handle_settings_window_event(
1392 &mut self,
1393 event: WindowEvent,
1394 ) -> Option<SettingsWindowAction> {
1395 if let Some(settings_window) = &mut self.settings_window {
1396 let action = settings_window.handle_window_event(event);
1397
1398 if settings_window.should_close() {
1400 self.close_settings_window();
1401 return Some(SettingsWindowAction::Close);
1402 }
1403
1404 return Some(action);
1405 }
1406 None
1407 }
1408
1409 pub fn apply_config_to_windows(&mut self, config: &Config) {
1411 use crate::app::config_updates::ConfigChanges;
1412
1413 crate::debug::set_log_level(config.log_level.to_level_filter());
1415
1416 let mut last_shader_result: Option<Option<String>> = None;
1419 let mut last_cursor_shader_result: Option<Option<String>> = None;
1420
1421 for window_state in self.windows.values_mut() {
1422 let changes = ConfigChanges::detect(&window_state.config, config);
1424
1425 window_state.config = config.clone();
1427
1428 if changes.keybindings {
1430 window_state.keybinding_registry =
1431 crate::keybindings::KeybindingRegistry::from_config(&config.keybindings);
1432 log::info!(
1433 "Keybinding registry rebuilt with {} bindings",
1434 config.keybindings.len()
1435 );
1436 }
1437
1438 if changes.ai_inspector_auto_approve
1440 && let Some(agent) = &window_state.agent
1441 {
1442 let agent = agent.clone();
1443 let auto_approve = config.ai_inspector_auto_approve;
1444 let mode = if auto_approve {
1445 "bypassPermissions"
1446 } else {
1447 "default"
1448 }
1449 .to_string();
1450 window_state.runtime.spawn(async move {
1451 let agent = agent.lock().await;
1452 agent
1453 .auto_approve
1454 .store(auto_approve, std::sync::atomic::Ordering::Relaxed);
1455 if let Err(e) = agent.set_mode(&mode).await {
1456 log::error!("ACP: failed to set mode '{mode}': {e}");
1457 }
1458 });
1459 }
1460
1461 let (shader_result, cursor_result) = if let Some(renderer) = &mut window_state.renderer
1463 {
1464 renderer.update_opacity(config.window_opacity);
1466
1467 if changes.transparency_mode {
1469 renderer.set_transparency_affects_only_default_background(
1470 config.transparency_affects_only_default_background,
1471 );
1472 window_state.needs_redraw = true;
1473 }
1474
1475 if changes.keep_text_opaque {
1477 renderer.set_keep_text_opaque(config.keep_text_opaque);
1478 window_state.needs_redraw = true;
1479 }
1480
1481 if changes.link_underline_style {
1482 renderer.set_link_underline_style(config.link_underline_style);
1483 window_state.needs_redraw = true;
1484 }
1485
1486 if changes.vsync_mode {
1488 let (actual_mode, _changed) = renderer.update_vsync_mode(config.vsync_mode);
1489 if actual_mode != config.vsync_mode {
1491 window_state.config.vsync_mode = actual_mode;
1492 log::warn!(
1493 "Vsync mode {:?} is not supported. Using {:?} instead.",
1494 config.vsync_mode,
1495 actual_mode
1496 );
1497 }
1498 }
1499
1500 renderer.update_scrollbar_appearance(
1502 config.scrollbar_width,
1503 config.scrollbar_thumb_color,
1504 config.scrollbar_track_color,
1505 );
1506
1507 if changes.cursor_color {
1509 renderer.update_cursor_color(config.cursor_color);
1510 }
1511
1512 if changes.cursor_text_color {
1514 renderer.update_cursor_text_color(config.cursor_text_color);
1515 }
1516
1517 if changes.cursor_style || changes.cursor_blink {
1519 use crate::config::CursorStyle as ConfigCursorStyle;
1520 use par_term_emu_core_rust::cursor::CursorStyle as TermCursorStyle;
1521
1522 let term_style = if config.cursor_blink {
1523 match config.cursor_style {
1524 ConfigCursorStyle::Block => TermCursorStyle::BlinkingBlock,
1525 ConfigCursorStyle::Beam => TermCursorStyle::BlinkingBar,
1526 ConfigCursorStyle::Underline => TermCursorStyle::BlinkingUnderline,
1527 }
1528 } else {
1529 match config.cursor_style {
1530 ConfigCursorStyle::Block => TermCursorStyle::SteadyBlock,
1531 ConfigCursorStyle::Beam => TermCursorStyle::SteadyBar,
1532 ConfigCursorStyle::Underline => TermCursorStyle::SteadyUnderline,
1533 }
1534 };
1535
1536 for tab in window_state.tab_manager.tabs_mut() {
1537 if let Ok(mut term) = tab.terminal.try_lock() {
1538 term.set_cursor_style(term_style);
1539 }
1540 tab.cache.cells = None; }
1542 window_state.needs_redraw = true;
1543 }
1544
1545 if changes.cursor_enhancements {
1547 renderer.update_cursor_guide(
1548 config.cursor_guide_enabled,
1549 config.cursor_guide_color,
1550 );
1551 renderer.update_cursor_shadow(
1552 config.cursor_shadow_enabled,
1553 config.cursor_shadow_color,
1554 config.cursor_shadow_offset,
1555 config.cursor_shadow_blur,
1556 );
1557 renderer.update_cursor_boost(config.cursor_boost, config.cursor_boost_color);
1558 renderer.update_unfocused_cursor_style(config.unfocused_cursor_style);
1559 window_state.needs_redraw = true;
1560 }
1561
1562 if changes.command_separator {
1564 renderer.update_command_separator(
1565 config.command_separator_enabled,
1566 config.command_separator_thickness,
1567 config.command_separator_opacity,
1568 config.command_separator_exit_color,
1569 config.command_separator_color,
1570 );
1571 window_state.needs_redraw = true;
1572 }
1573
1574 if changes.any_bg_change() {
1576 let expanded_path = config.background_image.as_ref().map(|p| {
1578 if let Some(rest) = p.strip_prefix("~/")
1579 && let Some(home) = dirs::home_dir()
1580 {
1581 return home.join(rest).to_string_lossy().to_string();
1582 }
1583 p.clone()
1584 });
1585 renderer.set_background(
1586 config.background_mode,
1587 config.background_color,
1588 expanded_path.as_deref(),
1589 config.background_image_mode,
1590 config.background_image_opacity,
1591 config.background_image_enabled,
1592 );
1593 window_state.needs_redraw = true;
1594 }
1595
1596 if changes.pane_backgrounds {
1598 for pb_config in &config.pane_backgrounds {
1600 if let Err(e) = renderer.load_pane_background(&pb_config.image) {
1601 log::error!(
1602 "Failed to load pane {} background '{}': {}",
1603 pb_config.index,
1604 pb_config.image,
1605 e
1606 );
1607 }
1608 }
1609
1610 for tab in window_state.tab_manager.tabs_mut() {
1611 if let Some(pm) = tab.pane_manager_mut() {
1612 let panes = pm.all_panes_mut();
1613 for (index, pane) in panes.into_iter().enumerate() {
1614 if let Some((image_path, mode, opacity)) =
1615 config.get_pane_background(index)
1616 {
1617 let bg = crate::pane::PaneBackground {
1618 image_path: Some(image_path),
1619 mode,
1620 opacity,
1621 };
1622 pane.set_background(bg);
1623 } else {
1624 pane.set_background(crate::pane::PaneBackground::new());
1626 }
1627 }
1628 }
1629 }
1630 renderer.mark_dirty();
1631 window_state.needs_redraw = true;
1632 }
1633
1634 if changes.image_scaling_mode {
1636 renderer.update_image_scaling_mode(config.image_scaling_mode);
1637 window_state.needs_redraw = true;
1638 }
1639 if changes.image_preserve_aspect_ratio {
1640 renderer.update_image_preserve_aspect_ratio(config.image_preserve_aspect_ratio);
1641 window_state.needs_redraw = true;
1642 }
1643
1644 if changes.theme
1646 && let Some(tab) = window_state.tab_manager.active_tab()
1647 && let Ok(mut term) = tab.terminal.try_lock()
1648 {
1649 term.set_theme(config.load_theme());
1650 }
1651
1652 if changes.answerback_string {
1654 let answerback = if config.answerback_string.is_empty() {
1655 None
1656 } else {
1657 Some(config.answerback_string.clone())
1658 };
1659 for tab in window_state.tab_manager.tabs_mut() {
1660 if let Ok(term) = tab.terminal.try_lock() {
1661 term.set_answerback_string(answerback.clone());
1662 }
1663 }
1664 }
1665
1666 if changes.unicode_width {
1668 let width_config = par_term_emu_core_rust::WidthConfig::new(
1669 config.unicode_version,
1670 config.ambiguous_width,
1671 );
1672 for tab in window_state.tab_manager.tabs_mut() {
1673 if let Ok(term) = tab.terminal.try_lock() {
1674 term.set_width_config(width_config);
1675 }
1676 }
1677 }
1678
1679 if changes.normalization_form {
1681 for tab in window_state.tab_manager.tabs_mut() {
1682 if let Ok(term) = tab.terminal.try_lock() {
1683 term.set_normalization_form(config.normalization_form);
1684 }
1685 }
1686 }
1687
1688 let shader_override = config
1691 .custom_shader
1692 .as_ref()
1693 .and_then(|name| config.shader_configs.get(name));
1694 let metadata = config
1696 .custom_shader
1697 .as_ref()
1698 .and_then(|name| window_state.shader_metadata_cache.get(name).cloned());
1699 let resolved = resolve_shader_config(shader_override, metadata.as_ref(), config);
1700
1701 let shader_result =
1704 if changes.any_shader_change() || changes.shader_per_shader_config {
1705 log::info!(
1706 "SETTINGS: applying shader change: {:?} -> {:?}",
1707 window_state.config.custom_shader,
1708 config.custom_shader
1709 );
1710 Some(
1711 renderer
1712 .set_custom_shader_enabled(
1713 config.custom_shader_enabled,
1714 config.custom_shader.as_deref(),
1715 config.window_opacity,
1716 config.custom_shader_animation,
1717 resolved.animation_speed,
1718 resolved.full_content,
1719 resolved.brightness,
1720 &resolved.channel_paths(),
1721 resolved.cubemap_path().map(|p| p.as_path()),
1722 )
1723 .err(),
1724 )
1725 } else {
1726 None };
1728
1729 if changes.any_shader_change()
1733 || changes.shader_use_background_as_channel0
1734 || changes.any_bg_change()
1735 || changes.shader_per_shader_config
1736 {
1737 renderer.update_background_as_channel0_with_mode(
1738 resolved.use_background_as_channel0,
1739 config.background_mode,
1740 config.background_color,
1741 );
1742 }
1743
1744 let cursor_result = if changes.any_cursor_shader_toggle() {
1746 Some(
1747 renderer
1748 .set_cursor_shader_enabled(
1749 config.cursor_shader_enabled,
1750 config.cursor_shader.as_deref(),
1751 config.window_opacity,
1752 config.cursor_shader_animation,
1753 config.cursor_shader_animation_speed,
1754 )
1755 .err(),
1756 )
1757 } else {
1758 None };
1760
1761 (shader_result, cursor_result)
1762 } else {
1763 (None, None)
1764 };
1765
1766 if let Some(result) = shader_result {
1769 last_shader_result = Some(result);
1770 }
1771 if let Some(result) = cursor_result {
1772 last_cursor_shader_result = Some(result);
1773 }
1774
1775 if changes.font_rendering {
1777 if let Some(renderer) = &mut window_state.renderer {
1778 let mut updated = false;
1779 updated |= renderer.update_font_antialias(config.font_antialias);
1780 updated |= renderer.update_font_hinting(config.font_hinting);
1781 updated |= renderer.update_font_thin_strokes(config.font_thin_strokes);
1782 updated |= renderer.update_minimum_contrast(config.minimum_contrast);
1783 if updated {
1784 window_state.needs_redraw = true;
1785 }
1786 } else {
1787 window_state.pending_font_rebuild = true;
1788 }
1789 }
1790
1791 if let Some(window) = &window_state.window {
1793 if changes.window_title || changes.show_window_number {
1796 let title = window_state.format_title(&window_state.config.window_title);
1797 window.set_title(&title);
1798 }
1799 if changes.window_decorations {
1800 window.set_decorations(config.window_decorations);
1801 }
1802 if changes.lock_window_size {
1803 window.set_resizable(!config.lock_window_size);
1804 log::info!("Window resizable set to: {}", !config.lock_window_size);
1805 }
1806 window.set_window_level(if config.window_always_on_top {
1807 winit::window::WindowLevel::AlwaysOnTop
1808 } else {
1809 winit::window::WindowLevel::Normal
1810 });
1811
1812 #[cfg(target_os = "macos")]
1814 if changes.blur {
1815 let blur_radius = if config.blur_enabled && config.window_opacity < 1.0 {
1816 config.blur_radius
1817 } else {
1818 0 };
1820 if let Err(e) = crate::macos_blur::set_window_blur(window, blur_radius) {
1821 log::warn!("Failed to set window blur: {}", e);
1822 }
1823 }
1824
1825 window.request_redraw();
1826 }
1827
1828 if changes.font {
1830 window_state.pending_font_rebuild = true;
1831 }
1832
1833 if changes.needs_watcher_reinit() {
1835 window_state.reinit_shader_watcher();
1836 }
1837
1838 if changes.max_fps
1840 && let Some(window) = &window_state.window
1841 {
1842 for tab in window_state.tab_manager.tabs_mut() {
1843 tab.stop_refresh_task();
1844 tab.start_refresh_task(
1845 Arc::clone(&window_state.runtime),
1846 Arc::clone(window),
1847 config.max_fps,
1848 );
1849 }
1850 log::info!("Restarted refresh tasks with max_fps={}", config.max_fps);
1851 }
1852
1853 if changes.badge {
1855 window_state.badge_state.update_config(config);
1856 window_state.badge_state.mark_dirty();
1857 }
1858
1859 window_state.status_bar_ui.sync_monitor_state(config);
1861
1862 let dpi_scale = window_state
1865 .renderer
1866 .as_ref()
1867 .map(|r| r.scale_factor())
1868 .unwrap_or(1.0);
1869 let divider_width = config.pane_divider_width.unwrap_or(2.0) * dpi_scale;
1870 for tab in window_state.tab_manager.tabs_mut() {
1871 if let Some(pm) = tab.pane_manager_mut() {
1872 pm.set_divider_width(divider_width);
1873 pm.set_divider_hit_width(config.pane_divider_hit_width * dpi_scale);
1874 }
1875 }
1876
1877 for tab in window_state.tab_manager.tabs() {
1879 if let Ok(term) = tab.terminal.try_lock() {
1880 term.sync_triggers(&config.triggers);
1881 }
1882 }
1883
1884 if let Some(tab) = window_state.tab_manager.active_tab_mut() {
1886 tab.cache.cells = None;
1887 }
1888 window_state.needs_redraw = true;
1889 }
1890
1891 let dynamic_sources_changed =
1893 self.config.dynamic_profile_sources != config.dynamic_profile_sources;
1894
1895 self.config = config.clone();
1897
1898 if dynamic_sources_changed {
1900 self.dynamic_profile_manager.stop();
1901 if !config.dynamic_profile_sources.is_empty() {
1902 self.dynamic_profile_manager
1903 .start(&config.dynamic_profile_sources, &self.runtime);
1904 }
1905 log::info!(
1906 "Dynamic profile manager restarted with {} sources",
1907 config.dynamic_profile_sources.len()
1908 );
1909 }
1910
1911 if let Some(settings_window) = &mut self.settings_window {
1913 if let Some(result) = last_shader_result {
1914 settings_window.set_shader_error(result);
1915 }
1916 if let Some(result) = last_cursor_shader_result {
1917 settings_window.set_cursor_shader_error(result);
1918 }
1919 }
1920 }
1921
1922 pub fn apply_shader_from_editor(&mut self, source: &str) -> Result<(), String> {
1924 let mut last_error = None;
1925
1926 for window_state in self.windows.values_mut() {
1927 if let Some(renderer) = &mut window_state.renderer {
1928 match renderer.reload_shader_from_source(source) {
1929 Ok(()) => {
1930 window_state.needs_redraw = true;
1931 if let Some(window) = &window_state.window {
1932 window.request_redraw();
1933 }
1934 }
1935 Err(e) => {
1936 last_error = Some(format!("{:#}", e));
1937 }
1938 }
1939 }
1940 }
1941
1942 if let Some(settings_window) = &mut self.settings_window {
1944 if let Some(ref err) = last_error {
1945 settings_window.set_shader_error(Some(err.clone()));
1946 } else {
1947 settings_window.clear_shader_error();
1948 }
1949 }
1950
1951 last_error.map_or(Ok(()), Err)
1952 }
1953
1954 pub fn apply_cursor_shader_from_editor(&mut self, source: &str) -> Result<(), String> {
1956 let mut last_error = None;
1957
1958 for window_state in self.windows.values_mut() {
1959 if let Some(renderer) = &mut window_state.renderer {
1960 match renderer.reload_cursor_shader_from_source(source) {
1961 Ok(()) => {
1962 window_state.needs_redraw = true;
1963 if let Some(window) = &window_state.window {
1964 window.request_redraw();
1965 }
1966 }
1967 Err(e) => {
1968 last_error = Some(format!("{:#}", e));
1969 }
1970 }
1971 }
1972 }
1973
1974 if let Some(settings_window) = &mut self.settings_window {
1976 if let Some(ref err) = last_error {
1977 settings_window.set_cursor_shader_error(Some(err.clone()));
1978 } else {
1979 settings_window.clear_cursor_shader_error();
1980 }
1981 }
1982
1983 last_error.map_or(Ok(()), Err)
1984 }
1985
1986 pub fn request_settings_redraw(&self) {
1988 if let Some(settings_window) = &self.settings_window {
1989 settings_window.request_redraw();
1990 }
1991 }
1992
1993 pub fn start_coprocess(&mut self, config_index: usize) {
1995 log::debug!("start_coprocess called with index {}", config_index);
1996 let focused = self.get_focused_window_id();
1997 if let Some(window_id) = focused
1998 && let Some(ws) = self.windows.get_mut(&window_id)
1999 && let Some(tab) = ws.tab_manager.active_tab_mut()
2000 {
2001 if config_index >= ws.config.coprocesses.len() {
2002 log::warn!("Coprocess config index {} out of range", config_index);
2003 return;
2004 }
2005 let coproc_config = &ws.config.coprocesses[config_index];
2006 let core_config = par_term_emu_core_rust::coprocess::CoprocessConfig {
2007 command: coproc_config.command.clone(),
2008 args: coproc_config.args.clone(),
2009 cwd: None,
2010 env: crate::terminal::coprocess_env(),
2011 copy_terminal_output: coproc_config.copy_terminal_output,
2012 restart_policy: coproc_config.restart_policy.to_core(),
2013 restart_delay_ms: coproc_config.restart_delay_ms,
2014 };
2015 let term = tab.terminal.blocking_lock();
2017 match term.start_coprocess(core_config) {
2018 Ok(id) => {
2019 log::info!("Started coprocess '{}' (id={})", coproc_config.name, id);
2020 while tab.coprocess_ids.len() <= config_index {
2022 tab.coprocess_ids.push(None);
2023 }
2024 tab.coprocess_ids[config_index] = Some(id);
2025 }
2026 Err(e) => {
2027 let err_msg = format!("Failed to start: {}", e);
2028 log::error!("Failed to start coprocess '{}': {}", coproc_config.name, e);
2029 if let Some(sw) = &mut self.settings_window {
2031 let errors = &mut sw.settings_ui.coprocess_errors;
2032 while errors.len() <= config_index {
2033 errors.push(String::new());
2034 }
2035 errors[config_index] = err_msg;
2036 sw.request_redraw();
2037 }
2038 return;
2039 }
2040 }
2041 drop(term);
2042 self.sync_coprocess_running_state();
2044 } else {
2045 log::warn!("start_coprocess: no focused window or active tab found");
2046 }
2047 }
2048
2049 pub fn stop_coprocess(&mut self, config_index: usize) {
2051 log::debug!("stop_coprocess called with index {}", config_index);
2052 let focused = self.get_focused_window_id();
2053 if let Some(window_id) = focused
2054 && let Some(ws) = self.windows.get_mut(&window_id)
2055 && let Some(tab) = ws.tab_manager.active_tab_mut()
2056 {
2057 if let Some(Some(id)) = tab.coprocess_ids.get(config_index).copied() {
2058 let term = tab.terminal.blocking_lock();
2060 if let Err(e) = term.stop_coprocess(id) {
2061 log::error!("Failed to stop coprocess at index {}: {}", config_index, e);
2062 } else {
2063 log::info!("Stopped coprocess at index {} (id={})", config_index, id);
2064 }
2065 drop(term);
2066 tab.coprocess_ids[config_index] = None;
2067 }
2068 self.sync_coprocess_running_state();
2070 }
2071 }
2072
2073 const COPROCESS_OUTPUT_MAX_LINES: usize = 200;
2075
2076 pub fn sync_coprocess_running_state(&mut self) {
2078 let focused = self.get_focused_window_id();
2079 let (running_state, error_state, new_output): (Vec<bool>, Vec<String>, Vec<Vec<String>>) =
2080 if let Some(window_id) = focused
2081 && let Some(ws) = self.windows.get(&window_id)
2082 && let Some(tab) = ws.tab_manager.active_tab()
2083 {
2084 if let Ok(term) = tab.terminal.try_lock() {
2085 let mut running = Vec::new();
2086 let mut errors = Vec::new();
2087 let mut output = Vec::new();
2088 for (i, _) in ws.config.coprocesses.iter().enumerate() {
2089 let has_id = tab.coprocess_ids.get(i).and_then(|opt| opt.as_ref());
2090 let is_running =
2091 has_id.is_some_and(|id| term.coprocess_status(*id).unwrap_or(false));
2092 let err_text = if let Some(id) = has_id {
2095 if is_running {
2096 String::new()
2097 } else {
2098 term.read_coprocess_errors(*id)
2099 .unwrap_or_default()
2100 .join("\n")
2101 }
2102 } else if let Some(sw) = &self.settings_window
2103 && let Some(existing) = sw.settings_ui.coprocess_errors.get(i)
2104 && !existing.is_empty()
2105 {
2106 existing.clone()
2107 } else {
2108 String::new()
2109 };
2110 let lines = if let Some(id) = has_id {
2112 term.read_from_coprocess(*id).unwrap_or_default()
2113 } else {
2114 Vec::new()
2115 };
2116 running.push(is_running);
2117 errors.push(err_text);
2118 output.push(lines);
2119 }
2120 (running, errors, output)
2121 } else {
2122 (Vec::new(), Vec::new(), Vec::new())
2123 }
2124 } else {
2125 (Vec::new(), Vec::new(), Vec::new())
2126 };
2127 if let Some(sw) = &mut self.settings_window {
2128 let running_changed = sw.settings_ui.coprocess_running != running_state;
2129 let errors_changed = sw.settings_ui.coprocess_errors != error_state;
2130 let has_new_output = new_output.iter().any(|lines| !lines.is_empty());
2131
2132 let count = running_state.len();
2134 sw.settings_ui.coprocess_output.resize_with(count, Vec::new);
2135 sw.settings_ui
2136 .coprocess_output_expanded
2137 .resize(count, false);
2138
2139 for (i, lines) in new_output.into_iter().enumerate() {
2141 if !lines.is_empty() {
2142 let buf = &mut sw.settings_ui.coprocess_output[i];
2143 buf.extend(lines);
2144 let overflow = buf.len().saturating_sub(Self::COPROCESS_OUTPUT_MAX_LINES);
2145 if overflow > 0 {
2146 buf.drain(..overflow);
2147 }
2148 }
2149 }
2150
2151 if running_changed || errors_changed || has_new_output {
2152 sw.settings_ui.coprocess_running = running_state;
2153 sw.settings_ui.coprocess_errors = error_state;
2154 sw.request_redraw();
2155 }
2156 }
2157 }
2158
2159 pub fn start_script(&mut self, config_index: usize) {
2165 crate::debug_info!(
2166 "SCRIPT",
2167 "start_script called with config_index={}",
2168 config_index
2169 );
2170 let focused = self.get_focused_window_id();
2171 if let Some(window_id) = focused
2172 && let Some(ws) = self.windows.get_mut(&window_id)
2173 && let Some(tab) = ws.tab_manager.active_tab_mut()
2174 {
2175 crate::debug_info!(
2176 "SCRIPT",
2177 "start_script: ws.config.scripts.len()={}, tab.script_ids.len()={}",
2178 ws.config.scripts.len(),
2179 tab.script_ids.len()
2180 );
2181 if config_index >= ws.config.scripts.len() {
2182 crate::debug_error!(
2183 "SCRIPT",
2184 "Script config index {} out of range (scripts.len={})",
2185 config_index,
2186 ws.config.scripts.len()
2187 );
2188 return;
2189 }
2190 let script_config = &ws.config.scripts[config_index];
2191 crate::debug_info!(
2192 "SCRIPT",
2193 "start_script: found config name='{}' path='{}' enabled={} args={:?}",
2194 script_config.name,
2195 script_config.script_path,
2196 script_config.enabled,
2197 script_config.args
2198 );
2199 if !script_config.enabled {
2200 crate::debug_info!(
2201 "SCRIPT",
2202 "Script '{}' is disabled, not starting",
2203 script_config.name
2204 );
2205 return;
2206 }
2207
2208 let subscription_filter = if script_config.subscriptions.is_empty() {
2210 None
2211 } else {
2212 Some(
2213 script_config
2214 .subscriptions
2215 .iter()
2216 .cloned()
2217 .collect::<std::collections::HashSet<String>>(),
2218 )
2219 };
2220
2221 let forwarder = std::sync::Arc::new(
2223 crate::scripting::observer::ScriptEventForwarder::new(subscription_filter),
2224 );
2225
2226 let observer_id = {
2228 let term = tab.terminal.blocking_lock();
2229 term.add_observer(forwarder.clone())
2230 };
2231
2232 crate::debug_info!("SCRIPT", "start_script: spawning process...");
2234 match tab.script_manager.start_script(script_config) {
2235 Ok(script_id) => {
2236 crate::debug_info!(
2237 "SCRIPT",
2238 "start_script: SUCCESS script_id={} observer_id={:?}",
2239 script_id,
2240 observer_id
2241 );
2242
2243 while tab.script_ids.len() <= config_index {
2245 tab.script_ids.push(None);
2246 }
2247 while tab.script_observer_ids.len() <= config_index {
2248 tab.script_observer_ids.push(None);
2249 }
2250 while tab.script_forwarders.len() <= config_index {
2251 tab.script_forwarders.push(None);
2252 }
2253
2254 tab.script_ids[config_index] = Some(script_id);
2255 tab.script_observer_ids[config_index] = Some(observer_id);
2256 tab.script_forwarders[config_index] = Some(forwarder);
2257 }
2258 Err(e) => {
2259 let err_msg = format!("Failed to start: {}", e);
2260 crate::debug_error!(
2261 "SCRIPT",
2262 "start_script: FAILED to start '{}': {}",
2263 script_config.name,
2264 e
2265 );
2266
2267 let term = tab.terminal.blocking_lock();
2269 term.remove_observer(observer_id);
2270 drop(term);
2271
2272 if let Some(sw) = &mut self.settings_window {
2274 let errors = &mut sw.settings_ui.script_errors;
2275 while errors.len() <= config_index {
2276 errors.push(String::new());
2277 }
2278 errors[config_index] = err_msg;
2279 sw.request_redraw();
2280 }
2281 return;
2282 }
2283 }
2284 self.sync_script_running_state();
2286 } else {
2287 crate::debug_error!(
2288 "SCRIPT",
2289 "start_script: no focused window or active tab found"
2290 );
2291 }
2292 }
2293
2294 pub fn stop_script(&mut self, config_index: usize) {
2296 log::debug!("stop_script called with index {}", config_index);
2297 let focused = self.get_focused_window_id();
2298 if let Some(window_id) = focused
2299 && let Some(ws) = self.windows.get_mut(&window_id)
2300 && let Some(tab) = ws.tab_manager.active_tab_mut()
2301 {
2302 if let Some(Some(script_id)) = tab.script_ids.get(config_index).copied() {
2304 tab.script_manager.stop_script(script_id);
2305 log::info!(
2306 "Stopped script at index {} (id={})",
2307 config_index,
2308 script_id
2309 );
2310 }
2311
2312 if let Some(Some(observer_id)) = tab.script_observer_ids.get(config_index).copied() {
2314 let term = tab.terminal.blocking_lock();
2315 term.remove_observer(observer_id);
2316 drop(term);
2317 }
2318
2319 if let Some(slot) = tab.script_ids.get_mut(config_index) {
2321 *slot = None;
2322 }
2323 if let Some(slot) = tab.script_observer_ids.get_mut(config_index) {
2324 *slot = None;
2325 }
2326 if let Some(slot) = tab.script_forwarders.get_mut(config_index) {
2327 *slot = None;
2328 }
2329
2330 self.sync_script_running_state();
2332 }
2333 }
2334
2335 const SCRIPT_OUTPUT_MAX_LINES: usize = 200;
2337
2338 pub fn sync_script_running_state(&mut self) {
2343 let focused = self.get_focused_window_id();
2344
2345 #[allow(clippy::type_complexity)]
2347 let (running_state, error_state, new_output, panel_state): (
2348 Vec<bool>,
2349 Vec<String>,
2350 Vec<Vec<String>>,
2351 Vec<Option<(String, String)>>,
2352 ) = if let Some(window_id) = focused
2353 && let Some(ws) = self.windows.get_mut(&window_id)
2354 && let Some(tab) = ws.tab_manager.active_tab_mut()
2355 {
2356 let script_count = ws.config.scripts.len();
2357 let mut running = Vec::with_capacity(script_count);
2358 let mut errors = Vec::with_capacity(script_count);
2359 let mut output = Vec::with_capacity(script_count);
2360 let mut panels = Vec::with_capacity(script_count);
2361
2362 for i in 0..script_count {
2363 let has_script_id = tab.script_ids.get(i).and_then(|opt| *opt);
2364 let is_running = has_script_id.is_some_and(|id| tab.script_manager.is_running(id));
2365
2366 if is_running && let Some(Some(forwarder)) = tab.script_forwarders.get(i) {
2368 let events = forwarder.drain_events();
2369 if let Some(script_id) = has_script_id {
2370 for event in &events {
2371 let _ = tab.script_manager.send_event(script_id, event);
2372 }
2373 }
2374 }
2375
2376 let mut log_lines = Vec::new();
2378 let mut panel_val = tab
2379 .script_manager
2380 .get_panel(has_script_id.unwrap_or(0))
2381 .cloned();
2382
2383 if let Some(script_id) = has_script_id {
2384 let commands = tab.script_manager.read_commands(script_id);
2385 for cmd in commands {
2386 match cmd {
2387 crate::scripting::protocol::ScriptCommand::Log { level, message } => {
2388 log_lines.push(format!("[{}] {}", level, message));
2389 }
2390 crate::scripting::protocol::ScriptCommand::SetPanel {
2391 title,
2392 content,
2393 } => {
2394 tab.script_manager.set_panel(
2395 script_id,
2396 title.clone(),
2397 content.clone(),
2398 );
2399 panel_val = Some((title, content));
2400 }
2401 crate::scripting::protocol::ScriptCommand::ClearPanel {} => {
2402 tab.script_manager.clear_panel(script_id);
2403 panel_val = None;
2404 }
2405 _ => {
2410 log::debug!("Script command not yet implemented: {:?}", cmd);
2411 }
2412 }
2413 }
2414 }
2415
2416 let err_text = if let Some(script_id) = has_script_id {
2418 if is_running {
2419 let err_lines = tab.script_manager.read_errors(script_id);
2421 if !err_lines.is_empty() {
2422 err_lines.join("\n")
2423 } else {
2424 String::new()
2425 }
2426 } else {
2427 let err_lines = tab.script_manager.read_errors(script_id);
2428 err_lines.join("\n")
2429 }
2430 } else if let Some(sw) = &self.settings_window
2431 && let Some(existing) = sw.settings_ui.script_errors.get(i)
2432 && !existing.is_empty()
2433 {
2434 existing.clone()
2435 } else {
2436 String::new()
2437 };
2438
2439 running.push(is_running);
2440 errors.push(err_text);
2441 output.push(log_lines);
2442 panels.push(panel_val);
2443 }
2444
2445 (running, errors, output, panels)
2446 } else {
2447 (Vec::new(), Vec::new(), Vec::new(), Vec::new())
2448 };
2449
2450 if let Some(sw) = &mut self.settings_window {
2452 let running_changed = sw.settings_ui.script_running != running_state;
2453 let errors_changed = sw.settings_ui.script_errors != error_state;
2454 let has_new_output = new_output.iter().any(|lines| !lines.is_empty());
2455 let panels_changed = sw.settings_ui.script_panels != panel_state;
2456
2457 if running_changed || errors_changed {
2458 crate::debug_info!(
2459 "SCRIPT",
2460 "sync: state change - running={:?} errors_changed={}",
2461 running_state,
2462 errors_changed
2463 );
2464 }
2465
2466 let count = running_state.len();
2467 sw.settings_ui.script_output.resize_with(count, Vec::new);
2468 sw.settings_ui.script_output_expanded.resize(count, false);
2469 sw.settings_ui.script_panels.resize_with(count, || None);
2470
2471 for (i, lines) in new_output.into_iter().enumerate() {
2473 if !lines.is_empty() {
2474 let buf = &mut sw.settings_ui.script_output[i];
2475 buf.extend(lines);
2476 let overflow = buf.len().saturating_sub(Self::SCRIPT_OUTPUT_MAX_LINES);
2477 if overflow > 0 {
2478 buf.drain(..overflow);
2479 }
2480 }
2481 }
2482
2483 if running_changed || errors_changed || has_new_output || panels_changed {
2484 sw.settings_ui.script_running = running_state;
2485 sw.settings_ui.script_errors = error_state;
2486 sw.settings_ui.script_panels = panel_state;
2487 sw.request_redraw();
2488 }
2489 }
2490 }
2491
2492 pub fn create_window_with_overrides(
2502 &mut self,
2503 event_loop: &ActiveEventLoop,
2504 position: (i32, i32),
2505 size: (u32, u32),
2506 tab_cwds: &[Option<String>],
2507 active_tab_index: usize,
2508 ) {
2509 use winit::window::Window;
2510
2511 if let Ok(fresh_config) = Config::load() {
2513 self.config = fresh_config;
2514 }
2515
2516 let window_number = self.windows.len() + 1;
2518 let title = if self.config.show_window_number {
2519 format!("{} [{}]", self.config.window_title, window_number)
2520 } else {
2521 self.config.window_title.clone()
2522 };
2523
2524 let mut window_attrs = Window::default_attributes()
2525 .with_title(&title)
2526 .with_inner_size(winit::dpi::PhysicalSize::new(size.0, size.1))
2527 .with_position(winit::dpi::PhysicalPosition::new(position.0, position.1))
2528 .with_decorations(self.config.window_decorations);
2529
2530 if self.config.lock_window_size {
2531 window_attrs = window_attrs.with_resizable(false);
2532 }
2533
2534 let icon_bytes = include_bytes!("../../assets/icon.png");
2536 if let Ok(icon_image) = image::load_from_memory(icon_bytes) {
2537 let rgba = icon_image.to_rgba8();
2538 let (w, h) = rgba.dimensions();
2539 if let Ok(icon) = winit::window::Icon::from_rgba(rgba.into_raw(), w, h) {
2540 window_attrs = window_attrs.with_window_icon(Some(icon));
2541 }
2542 }
2543
2544 if self.config.window_always_on_top {
2545 window_attrs = window_attrs.with_window_level(winit::window::WindowLevel::AlwaysOnTop);
2546 }
2547
2548 window_attrs = window_attrs.with_transparent(true);
2549
2550 match event_loop.create_window(window_attrs) {
2551 Ok(window) => {
2552 let window_id = window.id();
2553 let mut window_state =
2554 WindowState::new(self.config.clone(), Arc::clone(&self.runtime));
2555 window_state.window_index = window_number;
2556
2557 let runtime = Arc::clone(&self.runtime);
2558 if let Err(e) = runtime.block_on(window_state.initialize_async(window)) {
2559 log::error!("Failed to initialize arranged window: {}", e);
2560 return;
2561 }
2562
2563 if self.menu.is_none() {
2565 match MenuManager::new() {
2566 Ok(menu) => {
2567 if let Some(win) = &window_state.window
2568 && let Err(e) = menu.init_for_window(win)
2569 {
2570 log::warn!("Failed to initialize menu: {}", e);
2571 }
2572 self.menu = Some(menu);
2573 }
2574 Err(e) => {
2575 log::warn!("Failed to create menu: {}", e);
2576 }
2577 }
2578 } else if let Some(menu) = &self.menu
2579 && let Some(win) = &window_state.window
2580 && let Err(e) = menu.init_for_window(win)
2581 {
2582 log::warn!("Failed to initialize menu for window: {}", e);
2583 }
2584
2585 if let Some(win) = &window_state.window {
2587 win.set_outer_position(winit::dpi::PhysicalPosition::new(
2588 position.0, position.1,
2589 ));
2590 }
2591
2592 if let Some(first_cwd) = tab_cwds.first().and_then(|c| c.as_ref())
2595 && let Some(tab) = window_state.tab_manager.active_tab_mut()
2596 {
2597 tab.working_directory = Some(first_cwd.clone());
2598 }
2599
2600 let grid_size = window_state.renderer.as_ref().map(|r| r.grid_size());
2602 for cwd in tab_cwds.iter().skip(1) {
2603 if let Err(e) = window_state.tab_manager.new_tab_with_cwd(
2604 &self.config,
2605 Arc::clone(&self.runtime),
2606 cwd.clone(),
2607 grid_size,
2608 ) {
2609 log::warn!("Failed to create tab in arranged window: {}", e);
2610 }
2611 }
2612
2613 window_state
2615 .tab_manager
2616 .switch_to_index(active_tab_index + 1);
2617
2618 if let Some(win) = &window_state.window {
2620 for tab in window_state.tab_manager.tabs_mut() {
2621 tab.start_refresh_task(
2622 Arc::clone(&self.runtime),
2623 Arc::clone(win),
2624 self.config.max_fps,
2625 );
2626 }
2627 }
2628
2629 self.windows.insert(window_id, window_state);
2630 self.pending_window_count += 1;
2631
2632 if self.start_time.is_none() {
2633 self.start_time = Some(Instant::now());
2634 }
2635
2636 log::info!(
2637 "Created arranged window {:?} at ({}, {}) size {}x{} with {} tabs",
2638 window_id,
2639 position.0,
2640 position.1,
2641 size.0,
2642 size.1,
2643 tab_cwds.len().max(1),
2644 );
2645 }
2646 Err(e) => {
2647 log::error!("Failed to create arranged window: {}", e);
2648 }
2649 }
2650 }
2651
2652 pub fn save_arrangement(&mut self, name: String, event_loop: &ActiveEventLoop) {
2654 if let Some(existing) = self.arrangement_manager.find_by_name(&name) {
2656 let existing_id = existing.id;
2657 self.arrangement_manager.remove(&existing_id);
2658 log::info!("Overwriting existing arrangement '{}'", name);
2659 }
2660
2661 let arrangement =
2662 arrangements::capture::capture_arrangement(name.clone(), &self.windows, event_loop);
2663 log::info!(
2664 "Saved arrangement '{}' with {} windows",
2665 name,
2666 arrangement.windows.len()
2667 );
2668 self.arrangement_manager.add(arrangement);
2669 if let Err(e) = arrangements::storage::save_arrangements(&self.arrangement_manager) {
2670 log::error!("Failed to save arrangements: {}", e);
2671 }
2672 self.sync_arrangements_to_settings();
2673 }
2674
2675 pub fn restore_arrangement(&mut self, id: ArrangementId, event_loop: &ActiveEventLoop) {
2679 let arrangement = match self.arrangement_manager.get(&id) {
2680 Some(a) => a.clone(),
2681 None => {
2682 log::error!("Arrangement not found: {}", id);
2683 return;
2684 }
2685 };
2686
2687 log::info!(
2688 "Restoring arrangement '{}' ({} windows)",
2689 arrangement.name,
2690 arrangement.windows.len()
2691 );
2692
2693 let window_ids: Vec<WindowId> = self.windows.keys().copied().collect();
2695 for window_id in window_ids {
2696 if let Some(window_state) = self.windows.remove(&window_id) {
2697 drop(window_state);
2698 }
2699 }
2700
2701 let available_monitors: Vec<_> = event_loop.available_monitors().collect();
2703 let monitor_mapping = arrangements::restore::build_monitor_mapping(
2704 &arrangement.monitor_layout,
2705 &available_monitors,
2706 );
2707
2708 for (i, window_snapshot) in arrangement.windows.iter().enumerate() {
2710 let Some((x, y, w, h)) = arrangements::restore::compute_restore_position(
2711 window_snapshot,
2712 &monitor_mapping,
2713 &available_monitors,
2714 ) else {
2715 log::warn!("Could not compute position for window {} in arrangement", i);
2716 continue;
2717 };
2718
2719 let tab_cwds = arrangements::restore::tab_cwds(&arrangement, i);
2720 self.create_window_with_overrides(
2721 event_loop,
2722 (x, y),
2723 (w, h),
2724 &tab_cwds,
2725 window_snapshot.active_tab_index,
2726 );
2727 }
2728
2729 if self.windows.is_empty() {
2731 log::warn!("Arrangement had no restorable windows, creating default window");
2732 self.create_window(event_loop);
2733 }
2734 }
2735
2736 pub fn restore_arrangement_by_name(
2738 &mut self,
2739 name: &str,
2740 event_loop: &ActiveEventLoop,
2741 ) -> bool {
2742 if let Some(arrangement) = self.arrangement_manager.find_by_name(name) {
2743 let id = arrangement.id;
2744 self.restore_arrangement(id, event_loop);
2745 true
2746 } else {
2747 log::warn!("Arrangement not found by name: {}", name);
2748 false
2749 }
2750 }
2751
2752 pub fn delete_arrangement(&mut self, id: ArrangementId) {
2754 if let Some(removed) = self.arrangement_manager.remove(&id) {
2755 log::info!("Deleted arrangement '{}'", removed.name);
2756 if let Err(e) = arrangements::storage::save_arrangements(&self.arrangement_manager) {
2757 log::error!("Failed to save arrangements after delete: {}", e);
2758 }
2759 self.sync_arrangements_to_settings();
2760 }
2761 }
2762
2763 pub fn rename_arrangement(&mut self, id: ArrangementId, new_name: String) {
2765 if let Some(arrangement) = self.arrangement_manager.get_mut(&id) {
2766 log::info!(
2767 "Renamed arrangement '{}' -> '{}'",
2768 arrangement.name,
2769 new_name
2770 );
2771 arrangement.name = new_name;
2772 if let Err(e) = arrangements::storage::save_arrangements(&self.arrangement_manager) {
2773 log::error!("Failed to save arrangements after rename: {}", e);
2774 }
2775 self.sync_arrangements_to_settings();
2776 }
2777 }
2778
2779 pub fn move_arrangement_up(&mut self, id: ArrangementId) {
2781 self.arrangement_manager.move_up(&id);
2782 if let Err(e) = arrangements::storage::save_arrangements(&self.arrangement_manager) {
2783 log::error!("Failed to save arrangements after reorder: {}", e);
2784 }
2785 self.sync_arrangements_to_settings();
2786 }
2787
2788 pub fn move_arrangement_down(&mut self, id: ArrangementId) {
2790 self.arrangement_manager.move_down(&id);
2791 if let Err(e) = arrangements::storage::save_arrangements(&self.arrangement_manager) {
2792 log::error!("Failed to save arrangements after reorder: {}", e);
2793 }
2794 self.sync_arrangements_to_settings();
2795 }
2796
2797 pub fn sync_arrangements_to_settings(&mut self) {
2799 if let Some(sw) = &mut self.settings_window {
2800 sw.settings_ui.arrangement_manager = self.arrangement_manager.clone();
2801 }
2802 }
2803
2804 pub fn send_test_notification(&self) {
2805 log::info!("Sending test notification");
2806
2807 #[cfg(not(target_os = "macos"))]
2808 {
2809 use notify_rust::Notification;
2810 if let Err(e) = Notification::new()
2811 .summary("par-term Test Notification")
2812 .body("If you see this, notifications are working!")
2813 .timeout(notify_rust::Timeout::Milliseconds(5000))
2814 .show()
2815 {
2816 log::warn!("Failed to send test notification: {}", e);
2817 }
2818 }
2819
2820 #[cfg(target_os = "macos")]
2821 {
2822 let script = r#"display notification "If you see this, notifications are working!" with title "par-term Test Notification""#;
2824
2825 if let Err(e) = std::process::Command::new("osascript")
2826 .arg("-e")
2827 .arg(script)
2828 .output()
2829 {
2830 log::warn!("Failed to send macOS test notification: {}", e);
2831 }
2832 }
2833 }
2834}