Skip to main content

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}