par_term/app/window_manager/mod.rs
1//! Multi-window manager for the terminal emulator
2//!
3//! This module contains `WindowManager`, which coordinates multiple terminal windows,
4//! handles the native menu system, and manages shared resources.
5//!
6//! The implementation is split across sub-modules for clarity:
7//! - `cli_timer` — timing-based CLI options (exit-after, screenshot, command send)
8//! - `update_checker` — periodic/forced update checks and desktop notifications
9//! - `window_close` — window teardown and resource reaping
10//! - `window_lifecycle` — window creation and monitor positioning
11//! - `window_session` — session save/restore and arranged-window creation
12//! - `menu_actions` — native menu event dispatch
13//! - `settings_actions` — settings window open/close, event routing, and live config propagation (R-27)
14//! - `coprocess` — coprocess start/stop and state sync to settings UI
15//! - `scripting` — script start/stop and state sync to settings UI
16//! - `arrangements` — save/restore/manage window arrangements
17//! - `config_propagation` — apply config changes from settings to all windows
18//! - `config_renderer_apply` — renderer-specific settings application (split from config_propagation)
19
20mod arrangements;
21mod cli_timer;
22mod config_propagation;
23mod config_renderer_apply;
24mod coprocess;
25mod menu_actions;
26mod scripting;
27mod settings_actions;
28mod update_checker;
29mod window_close;
30mod window_lifecycle;
31mod window_session;
32
33use crate::app::window_state::WindowState;
34use crate::arrangements::ArrangementManager;
35use crate::cli::RuntimeOptions;
36use crate::config::Config;
37use crate::menu::MenuManager;
38use crate::settings_window::SettingsWindow;
39use arc_swap::ArcSwap;
40use par_term_update::update_checker::{UpdateCheckResult, UpdateChecker};
41use std::collections::HashMap;
42use std::sync::Arc;
43use std::sync::mpsc;
44use std::time::Instant;
45use tokio::runtime::Runtime;
46use winit::window::WindowId;
47
48/// Destination for a `move_tab` operation.
49///
50/// `NewWindow` → spawn a fresh par-term window and insert the transferred tab as its only tab.
51/// `ExistingWindow(WindowId)` → append the transferred tab to an already-open window.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub(crate) enum MoveDestination {
54 NewWindow,
55 ExistingWindow(WindowId),
56}
57
58/// A request to move a tab, stashed on `WindowState::overlay_state` from the
59/// per-window action handler and drained by `WindowManager::about_to_wait`.
60#[derive(Debug, Clone, Copy)]
61pub(crate) struct MoveTabRequest {
62 pub(crate) tab_id: crate::tab::TabId,
63 pub(crate) destination: MoveDestination,
64}
65
66/// Manages multiple terminal windows and shared resources
67pub(crate) struct WindowManager {
68 /// Per-window state indexed by window ID
69 pub(crate) windows: HashMap<WindowId, WindowState>,
70 /// Native menu manager
71 pub(crate) menu: Option<MenuManager>,
72 /// Shared configuration (QA-001: ArcSwap for atomic config swaps across windows)
73 pub(crate) config: ArcSwap<Config>,
74 /// Shared async runtime
75 pub(crate) runtime: Arc<Runtime>,
76 /// Flag to indicate if app should exit
77 pub(crate) should_exit: bool,
78 /// Counter for generating unique window IDs during creation
79 pending_window_count: usize,
80 /// Separate settings window (if open)
81 pub(crate) settings_window: Option<SettingsWindow>,
82 /// Runtime options from CLI
83 pub(crate) runtime_options: RuntimeOptions,
84 /// When the app started (for timing-based CLI options)
85 pub(crate) start_time: Option<Instant>,
86 /// Whether the command has been sent
87 pub(crate) command_sent: bool,
88 /// Whether the screenshot has been taken
89 pub(crate) screenshot_taken: bool,
90 /// Update checker for checking GitHub releases. `Arc` so the blocking
91 /// `check_now` can run off the main thread via `runtime.spawn_blocking`.
92 pub(crate) update_checker: Arc<UpdateChecker>,
93 /// Time of next scheduled update check
94 pub(crate) next_update_check: Option<Instant>,
95 /// Last update check result (for display in settings)
96 pub(crate) last_update_result: Option<UpdateCheckResult>,
97 /// Delivers update-check results produced off the main thread.
98 pub(crate) update_check_tx: mpsc::Sender<update_checker::UpdateCheckOutcome>,
99 /// Drained on the main thread each frame to apply completed checks.
100 pub(crate) update_check_rx: mpsc::Receiver<update_checker::UpdateCheckOutcome>,
101 /// Saved window arrangement manager
102 pub(crate) arrangement_manager: ArrangementManager,
103 /// Whether auto-restore has been attempted this session
104 pub(crate) auto_restore_done: bool,
105 /// Dynamic profile manager for fetching remote profiles
106 pub(crate) dynamic_profile_manager: crate::profile::DynamicProfileManager,
107}
108
109impl WindowManager {
110 /// Create a new window manager
111 pub fn new(config: Config, runtime: Arc<Runtime>, runtime_options: RuntimeOptions) -> Self {
112 // Load saved arrangements
113 let arrangement_manager = match crate::arrangements::storage::load_arrangements() {
114 Ok(manager) => manager,
115 Err(e) => {
116 log::warn!("Failed to load arrangements: {}", e);
117 ArrangementManager::new()
118 }
119 };
120
121 let mut dynamic_profile_manager = crate::profile::DynamicProfileManager::new();
122 if !config.dynamic_profile_sources.is_empty() {
123 // Propagate the global `allow_http_profiles` flag into each source so
124 // `fetch_profiles_inner` can enforce or relax the HTTPS requirement.
125 let mut sources = config.dynamic_profile_sources.clone();
126 for src in &mut sources {
127 src.allow_http = config.security.allow_http_profiles;
128 }
129 dynamic_profile_manager.start(&sources, &runtime);
130 }
131
132 // Channel for delivering off-thread update-check results back to the
133 // main thread (see `check_for_updates`).
134 let (update_check_tx, update_check_rx) = mpsc::channel();
135
136 Self {
137 windows: HashMap::new(),
138 menu: None,
139 config: ArcSwap::from(Arc::new(config)),
140 runtime,
141 should_exit: false,
142 pending_window_count: 0,
143 settings_window: None,
144 runtime_options,
145 start_time: None,
146 command_sent: false,
147 screenshot_taken: false,
148 update_checker: Arc::new(UpdateChecker::new(env!("CARGO_PKG_VERSION"))),
149 next_update_check: None,
150 last_update_result: None,
151 update_check_tx,
152 update_check_rx,
153 arrangement_manager,
154 auto_restore_done: false,
155 dynamic_profile_manager,
156 }
157 }
158
159 /// Get the ID of the currently focused window.
160 /// Returns the window with `is_focused == true`, or falls back to the first window if none is focused.
161 pub fn get_focused_window_id(&self) -> Option<WindowId> {
162 // Find the window that has focus
163 for (window_id, window_state) in &self.windows {
164 if window_state.focus_state.is_focused {
165 return Some(*window_id);
166 }
167 }
168 // Fallback: return the first window if no window claims focus
169 self.windows.keys().next().copied()
170 }
171
172 /// Get the currently focused window's state.
173 ///
174 /// Prefer this over `self.windows.values().next()`: `windows` is a `HashMap`,
175 /// so iteration order is unspecified and "first entry" picks an arbitrary
176 /// window once more than one is open.
177 pub fn focused_window(&self) -> Option<&WindowState> {
178 let window_id = self.get_focused_window_id()?;
179 self.windows.get(&window_id)
180 }
181
182 /// Mutable counterpart of [`Self::focused_window`].
183 pub fn focused_window_mut(&mut self) -> Option<&mut WindowState> {
184 let window_id = self.get_focused_window_id()?;
185 self.windows.get_mut(&window_id)
186 }
187}