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        match self.tab_manager.new_tab_from_profile(
50            &self.config.load(),
51            Arc::clone(&self.runtime),
52            &profile,
53            grid_size,
54        ) {
55            Ok(tab_id) => {
56                if self.config.load().tabs.new_tab_position
57                    == crate::config::NewTabPosition::AfterActive
58                    && let Some(idx) = prior_active_idx
59                {
60                    self.tab_manager.move_tab_to_index(tab_id, idx + 1);
61                }
62
63                // Set profile icon on the new tab
64                if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
65                    tab.profile.profile_icon = profile.icon.clone();
66                }
67
68                // Start refresh task for the new tab and resize to match window
69                if let Some(window) = &self.window
70                    && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
71                {
72                    tab.start_refresh_task(
73                        Arc::clone(&self.runtime),
74                        Arc::clone(window),
75                        self.config.load().rendering.max_fps,
76                        self.config.load().power.inactive_tab_fps,
77                    );
78
79                    // Resize terminal to match current renderer dimensions
80                    // try_lock: intentional — duplicate tab initialization in sync event loop.
81                    // On miss: duplicate tab starts with default dimensions; corrected on next
82                    // Resized event.
83                    if let Some(renderer) = &self.renderer
84                        && let Ok(mut term) = tab.terminal.try_write()
85                    {
86                        let (cols, rows) = renderer.grid_size();
87                        let size = renderer.size();
88                        let width_px = size.width as usize;
89                        let height_px = size.height as usize;
90
91                        term.set_cell_dimensions(
92                            renderer.cell_width() as u32,
93                            renderer.cell_height() as u32,
94                        );
95                        if let Err(e) = term.resize_with_pixels(cols, rows, width_px, height_px) {
96                            crate::debug_error!(
97                                "TERMINAL",
98                                "resize_with_pixels failed (open_profile): {e}"
99                            );
100                        }
101                        log::info!(
102                            "Opened profile '{}' in tab {} ({}x{} at {}x{} px)",
103                            profile.name,
104                            tab_id,
105                            cols,
106                            rows,
107                            width_px,
108                            height_px
109                        );
110                    }
111                }
112
113                // Update badge and shader settings with profile information
114                self.apply_profile_badge(&profile);
115                self.apply_profile_shader_settings(&profile);
116
117                self.focus_state.needs_redraw = true;
118                self.request_redraw();
119
120                // Auto-connect tmux session if profile has one configured
121                if let Some(ref session_name) = profile.tmux_session_name
122                    && self.config.load().tmux.tmux_enabled
123                    && !self.is_gateway_active()
124                {
125                    match profile.tmux_connection_mode {
126                        par_term_config::TmuxConnectionMode::ControlMode => {
127                            if let Err(e) = self.initiate_tmux_gateway(Some(session_name)) {
128                                crate::debug_error!(
129                                    "TMUX",
130                                    "Profile tmux auto-connect failed: {}",
131                                    e
132                                );
133                            }
134                        }
135                        par_term_config::TmuxConnectionMode::Normal => {
136                            // Write plain tmux command directly to the PTY
137                            let cmd = format!(
138                                "{} new-session -A -s '{}'\n",
139                                self.config.load().tmux.tmux_path,
140                                session_name.replace('\'', "'\\''")
141                            );
142                            if let Some(tab) = self.tab_manager.active_tab_mut()
143                                && let Ok(term) = tab.terminal.try_read()
144                                && let Err(e) = term.write(cmd.as_bytes())
145                            {
146                                crate::debug_error!(
147                                    "TAB_ACTION",
148                                    "PTY write failed (tmux profile attach): {e}"
149                                );
150                            }
151                        }
152                    }
153                }
154            }
155            Err(e) => {
156                log::error!("Failed to open profile '{}': {}", profile.name, e);
157
158                // Show user-friendly error notification
159                let error_msg = e.to_string();
160                let (title, message) = if error_msg.contains("Unable to spawn")
161                    || error_msg.contains("No viable candidates")
162                {
163                    // Extract the command name from the error if possible
164                    let cmd = profile
165                        .command
166                        .as_deref()
167                        .unwrap_or("the configured command");
168                    (
169                        format!("Profile '{}' Failed", profile.name),
170                        format!(
171                            "Command '{}' not found. Check that it's installed and in your PATH.",
172                            cmd
173                        ),
174                    )
175                } else if error_msg.contains("No such file or directory") {
176                    (
177                        format!("Profile '{}' Failed", profile.name),
178                        format!(
179                            "Working directory not found: {}",
180                            profile.working_directory.as_deref().unwrap_or("(unknown)")
181                        ),
182                    )
183                } else {
184                    (
185                        format!("Profile '{}' Failed", profile.name),
186                        format!("Failed to start: {}", error_msg),
187                    )
188                };
189                self.deliver_notification(&title, &message);
190            }
191        }
192    }
193
194    /// Apply profile badge settings
195    ///
196    /// Updates the badge session variables and applies any profile-specific
197    /// badge configuration (format, color, font, margins, etc.).
198    pub(crate) fn apply_profile_badge(&mut self, profile: &crate::profile::Profile) {
199        // Update session.profile_name variable
200        {
201            let mut vars = self.badge_state.variables_mut();
202            vars.profile_name = profile.name.clone();
203        }
204
205        // Apply all profile badge settings (format, color, font, margins, etc.)
206        self.badge_state.apply_profile_settings(profile);
207
208        if profile.badge_text.is_some() {
209            crate::debug_info!(
210                "PROFILE",
211                "Applied profile badge settings: format='{}', color={:?}, alpha={}",
212                profile.badge_text.as_deref().unwrap_or(""),
213                profile.badge_color,
214                profile.badge_color_alpha.unwrap_or(0.0)
215            );
216        }
217
218        // Mark badge as dirty to trigger re-render
219        self.badge_state.mark_dirty();
220    }
221
222    /// Apply profile shader settings to the global renderer state.
223    pub(crate) fn apply_profile_shader_settings(&mut self, profile: &crate::profile::Profile) {
224        let mut changed = false;
225        if let Some(shader) = &profile.shader {
226            self.config.rcu(|old| {
227                let mut new = (**old).clone();
228                new.shader.custom_shader = Some(shader.clone());
229                std::sync::Arc::new(new)
230            });
231            self.config.rcu(|old| {
232                let mut new = (**old).clone();
233                new.shader.custom_shader_enabled = true;
234                std::sync::Arc::new(new)
235            });
236            changed = true;
237        }
238        if let Some(brightness) = profile.shader_brightness {
239            self.config.rcu(|old| {
240                let mut new = (**old).clone();
241                new.shader.custom_shader_brightness = brightness.clamp(0.05, 1.0);
242                std::sync::Arc::new(new)
243            });
244            changed = true;
245        }
246        if let Some(text_opacity) = profile.shader_text_opacity {
247            self.config.rcu(|old| {
248                let mut new = (**old).clone();
249                new.shader.custom_shader_text_opacity = text_opacity.clamp(0.0, 1.0);
250                std::sync::Arc::new(new)
251            });
252            changed = true;
253        }
254        if let Some(animation_speed) = profile.shader_animation_speed {
255            self.config.rcu(|old| {
256                let mut new = (**old).clone();
257                new.shader.custom_shader_animation_speed = animation_speed.clamp(0.0, 5.0);
258                std::sync::Arc::new(new)
259            });
260            changed = true;
261        }
262        if let Some(texture_set) = &profile.shader_texture_set {
263            self.config.rcu(|old| {
264                let mut new = (**old).clone();
265                new.shader.custom_shader_channel0 = texture_set[0].clone();
266                std::sync::Arc::new(new)
267            });
268            self.config.rcu(|old| {
269                let mut new = (**old).clone();
270                new.shader.custom_shader_channel1 = texture_set[1].clone();
271                std::sync::Arc::new(new)
272            });
273            self.config.rcu(|old| {
274                let mut new = (**old).clone();
275                new.shader.custom_shader_channel2 = texture_set[2].clone();
276                std::sync::Arc::new(new)
277            });
278            self.config.rcu(|old| {
279                let mut new = (**old).clone();
280                new.shader.custom_shader_channel3 = texture_set[3].clone();
281                std::sync::Arc::new(new)
282            });
283            changed = true;
284        }
285
286        if changed {
287            self.refresh_background_shader_renderer();
288            crate::debug_info!(
289                "PROFILE",
290                "Applied shader overrides for profile '{}'",
291                profile.name
292            );
293        }
294    }
295
296    /// Toggle the profile drawer visibility
297    pub fn toggle_profile_drawer(&mut self) {
298        self.overlay_ui.profile_drawer_ui.toggle();
299        self.focus_state.needs_redraw = true;
300        self.request_redraw();
301    }
302
303    /// Save profiles to disk
304    pub fn save_profiles(&self) {
305        if let Err(e) = profile_storage::save_profiles(&self.overlay_ui.profile_manager) {
306            log::error!("Failed to save profiles: {}", e);
307        }
308    }
309
310    /// Update profile manager from modal working copy
311    pub fn apply_profile_changes(&mut self, profiles: Vec<crate::profile::Profile>) {
312        self.overlay_ui.profile_manager = ProfileManager::from_profiles(profiles);
313        self.save_profiles();
314        // Signal that the profiles menu needs to be updated
315        self.overlay_state.profiles_menu_needs_update = true;
316    }
317}