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