Skip to main content

par_term/app/tab_ops/
profile_ops.rs

1//! Profile management operations: open, apply, and persist profiles.
2//!
3//! Automatic profile switching (hostname, SSH, directory) lives in
4//! `profile_auto_switch`.
5
6use std::sync::Arc;
7
8use crate::profile::{ProfileId, ProfileManager, storage as profile_storage};
9
10use super::super::window_state::WindowState;
11
12impl WindowState {
13    /// Open a new tab from a profile
14    pub fn open_profile(&mut self, profile_id: ProfileId) {
15        log::debug!("open_profile called with id: {:?}", profile_id);
16
17        // Check max tabs limit
18        if self.config.load().tabs.max_tabs > 0
19            && self.tab_manager.tab_count() >= self.config.load().tabs.max_tabs
20        {
21            log::warn!(
22                "Cannot open profile: max_tabs limit ({}) reached",
23                self.config.load().tabs.max_tabs
24            );
25            self.deliver_notification(
26                "Tab Limit Reached",
27                &format!(
28                    "Cannot open profile: maximum of {} tabs already open",
29                    self.config.load().tabs.max_tabs
30                ),
31            );
32            return;
33        }
34
35        let profile = match self.overlay_ui.profile_manager.get(&profile_id) {
36            Some(p) => p.clone(),
37            None => {
38                log::error!("Profile not found: {:?}", profile_id);
39                return;
40            }
41        };
42        log::debug!("Found profile: {}", profile.name);
43
44        // Get current grid size from renderer
45        let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
46
47        let prior_active_idx = self.tab_manager.active_tab_index();
48
49        // Auto-attach a par-mux session (create-or-attach) when the
50        // profile names one; the tmux block further down owns the window
51        // when a gateway session is active. The daemon connect/spawn runs
52        // off the event loop (begin/poll pair). This runs BEFORE the
53        // profile-tab decision: a mux-only profile opens no local tab —
54        // its tab arrives from the daemon's %window-add.
55        #[cfg(feature = "mux")]
56        let mux_only = {
57            let mux_only = profile.mux_session_name.is_some()
58                && profile.command.is_none()
59                && profile.tmux_session_name.is_none();
60            if let Some(ref mux_name) = profile.mux_session_name
61                && self.tmux_state.transport.is_none()
62                && !self.is_gateway_active()
63            {
64                self.begin_mux_session_attach(mux_name);
65            }
66            mux_only
67        };
68        #[cfg(not(feature = "mux"))]
69        let mux_only = false;
70
71        if !mux_only {
72            match self.tab_manager.new_tab_from_profile(
73                &self.config.load(),
74                Arc::clone(&self.runtime),
75                &profile,
76                grid_size,
77            ) {
78                Ok(tab_id) => {
79                    if self.config.load().tabs.new_tab_position
80                        == crate::config::NewTabPosition::AfterActive
81                        && let Some(idx) = prior_active_idx
82                    {
83                        self.tab_manager.move_tab_to_index(tab_id, idx + 1);
84                    }
85
86                    // Set profile icon on the new tab
87                    if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
88                        tab.profile.profile_icon = profile.icon.clone();
89                    }
90
91                    // Start refresh task for the new tab and resize to match window
92                    if let Some(window) = &self.window
93                        && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
94                    {
95                        tab.start_refresh_task(
96                            Arc::clone(&self.runtime),
97                            Arc::clone(window),
98                            self.config.load().rendering.max_fps,
99                            self.config.load().power.inactive_tab_fps,
100                        );
101
102                        // Resize terminal to match current renderer dimensions
103                        // try_lock: intentional — duplicate tab initialization in sync event loop.
104                        // On miss: duplicate tab starts with default dimensions; corrected on next
105                        // Resized event.
106                        if let Some(renderer) = &self.renderer
107                            && let Ok(mut term) = tab.terminal.try_write()
108                        {
109                            let (cols, rows) = renderer.grid_size();
110                            let size = renderer.size();
111                            let width_px = size.width as usize;
112                            let height_px = size.height as usize;
113
114                            term.set_cell_dimensions(
115                                renderer.cell_width() as u32,
116                                renderer.cell_height() as u32,
117                            );
118                            if let Err(e) = term.resize_with_pixels(cols, rows, width_px, height_px)
119                            {
120                                crate::debug_error!(
121                                    "TERMINAL",
122                                    "resize_with_pixels failed (open_profile): {e}"
123                                );
124                            }
125                            log::info!(
126                                "Opened profile '{}' in tab {} ({}x{} at {}x{} px)",
127                                profile.name,
128                                tab_id,
129                                cols,
130                                rows,
131                                width_px,
132                                height_px
133                            );
134                        }
135                    }
136
137                    // Update badge and shader settings with profile information
138                    self.apply_profile_badge(&profile);
139                    self.apply_profile_shader_settings(&profile);
140
141                    self.focus_state.needs_redraw = true;
142                    self.request_redraw();
143
144                    // Auto-connect tmux session if profile has one configured
145                    if let Some(ref session_name) = profile.tmux_session_name
146                        && self.config.load().tmux.tmux_enabled
147                        && !self.is_gateway_active()
148                    {
149                        match profile.tmux_connection_mode {
150                            par_term_config::TmuxConnectionMode::ControlMode => {
151                                if let Err(e) = self.initiate_tmux_gateway(Some(session_name)) {
152                                    crate::debug_error!(
153                                        "TMUX",
154                                        "Profile tmux auto-connect failed: {}",
155                                        e
156                                    );
157                                }
158                            }
159                            par_term_config::TmuxConnectionMode::Normal => {
160                                // Write plain tmux command directly to the PTY
161                                let cmd = format!(
162                                    "{} new-session -A -s '{}'\n",
163                                    self.config.load().tmux.tmux_path,
164                                    session_name.replace('\'', "'\\''")
165                                );
166                                if let Some(tab) = self.tab_manager.active_tab_mut()
167                                    && let Ok(term) = tab.terminal.try_read()
168                                    && let Err(e) = term.write(cmd.as_bytes())
169                                {
170                                    crate::debug_error!(
171                                        "TAB_ACTION",
172                                        "PTY write failed (tmux profile attach): {e}"
173                                    );
174                                }
175                            }
176                        }
177                    }
178                }
179                Err(e) => {
180                    log::error!("Failed to open profile '{}': {}", profile.name, e);
181
182                    // Show user-friendly error notification
183                    let error_msg = e.to_string();
184                    let (title, message) = if error_msg.contains("Unable to spawn")
185                        || error_msg.contains("No viable candidates")
186                    {
187                        // Extract the command name from the error if possible
188                        let cmd = profile
189                            .command
190                            .as_deref()
191                            .unwrap_or("the configured command");
192                        (
193                            format!("Profile '{}' Failed", profile.name),
194                            format!(
195                                "Command '{}' not found. Check that it's installed and in your PATH.",
196                                cmd
197                            ),
198                        )
199                    } else if error_msg.contains("No such file or directory") {
200                        (
201                            format!("Profile '{}' Failed", profile.name),
202                            format!(
203                                "Working directory not found: {}",
204                                profile.working_directory.as_deref().unwrap_or("(unknown)")
205                            ),
206                        )
207                    } else {
208                        (
209                            format!("Profile '{}' Failed", profile.name),
210                            format!("Failed to start: {}", error_msg),
211                        )
212                    };
213                    self.deliver_notification(&title, &message);
214                }
215            }
216        }
217    }
218
219    /// Apply profile badge settings
220    ///
221    /// Updates the badge session variables and applies any profile-specific
222    /// badge configuration (format, color, font, margins, etc.).
223    pub(crate) fn apply_profile_badge(&mut self, profile: &crate::profile::Profile) {
224        // Update session.profile_name variable
225        {
226            let mut vars = self.badge_state.variables_mut();
227            vars.profile_name = profile.name.clone();
228        }
229
230        // Apply all profile badge settings (format, color, font, margins, etc.)
231        self.badge_state.apply_profile_settings(profile);
232
233        if profile.badge_text.is_some() {
234            crate::debug_info!(
235                "PROFILE",
236                "Applied profile badge settings: format='{}', color={:?}, alpha={}",
237                profile.badge_text.as_deref().unwrap_or(""),
238                profile.badge_color,
239                profile.badge_color_alpha.unwrap_or(0.0)
240            );
241        }
242
243        // Mark badge as dirty to trigger re-render
244        self.badge_state.mark_dirty();
245    }
246
247    /// Apply profile shader settings to the global renderer state.
248    pub(crate) fn apply_profile_shader_settings(&mut self, profile: &crate::profile::Profile) {
249        let mut changed = false;
250        if let Some(shader) = &profile.shader {
251            self.config.rcu(|old| {
252                let mut new = (**old).clone();
253                new.shader.custom_shader = Some(shader.clone());
254                std::sync::Arc::new(new)
255            });
256            self.config.rcu(|old| {
257                let mut new = (**old).clone();
258                new.shader.custom_shader_enabled = true;
259                std::sync::Arc::new(new)
260            });
261            changed = true;
262        }
263        if let Some(brightness) = profile.shader_brightness {
264            self.config.rcu(|old| {
265                let mut new = (**old).clone();
266                new.shader.custom_shader_brightness = brightness.clamp(0.05, 1.0);
267                std::sync::Arc::new(new)
268            });
269            changed = true;
270        }
271        if let Some(text_opacity) = profile.shader_text_opacity {
272            self.config.rcu(|old| {
273                let mut new = (**old).clone();
274                new.shader.custom_shader_text_opacity = text_opacity.clamp(0.0, 1.0);
275                std::sync::Arc::new(new)
276            });
277            changed = true;
278        }
279        if let Some(animation_speed) = profile.shader_animation_speed {
280            self.config.rcu(|old| {
281                let mut new = (**old).clone();
282                new.shader.custom_shader_animation_speed = animation_speed.clamp(0.0, 5.0);
283                std::sync::Arc::new(new)
284            });
285            changed = true;
286        }
287        if let Some(texture_set) = &profile.shader_texture_set {
288            self.config.rcu(|old| {
289                let mut new = (**old).clone();
290                new.shader.custom_shader_channel0 = texture_set[0].clone();
291                std::sync::Arc::new(new)
292            });
293            self.config.rcu(|old| {
294                let mut new = (**old).clone();
295                new.shader.custom_shader_channel1 = texture_set[1].clone();
296                std::sync::Arc::new(new)
297            });
298            self.config.rcu(|old| {
299                let mut new = (**old).clone();
300                new.shader.custom_shader_channel2 = texture_set[2].clone();
301                std::sync::Arc::new(new)
302            });
303            self.config.rcu(|old| {
304                let mut new = (**old).clone();
305                new.shader.custom_shader_channel3 = texture_set[3].clone();
306                std::sync::Arc::new(new)
307            });
308            changed = true;
309        }
310
311        if changed {
312            self.refresh_background_shader_renderer();
313            crate::debug_info!(
314                "PROFILE",
315                "Applied shader overrides for profile '{}'",
316                profile.name
317            );
318        }
319    }
320
321    /// Toggle the profile drawer visibility
322    pub fn toggle_profile_drawer(&mut self) {
323        self.overlay_ui.profile_drawer_ui.toggle();
324        self.focus_state.needs_redraw = true;
325        self.request_redraw();
326    }
327
328    /// Save profiles to disk
329    pub fn save_profiles(&self) {
330        if let Err(e) = profile_storage::save_profiles(&self.overlay_ui.profile_manager) {
331            log::error!("Failed to save profiles: {}", e);
332        }
333    }
334
335    /// Update profile manager from modal working copy
336    pub fn apply_profile_changes(&mut self, profiles: Vec<crate::profile::Profile>) {
337        self.overlay_ui.profile_manager = ProfileManager::from_profiles(profiles);
338        self.save_profiles();
339        // Signal that the profiles menu needs to be updated
340        self.overlay_state.profiles_menu_needs_update = true;
341    }
342}