Skip to main content

par_term/
profile_modal_ui.rs

1//! Profile management modal UI using egui
2//!
3//! Provides a modal dialog for creating, editing, and managing profiles.
4
5use crate::profile::{Profile, ProfileId, ProfileManager};
6use crate::shell_detection;
7
8/// Curated emoji presets organized by category for the profile icon picker
9const EMOJI_PRESETS: &[(&str, &[&str])] = &[
10    (
11        "Terminal",
12        &["๐Ÿ’ป", "๐Ÿ–ฅ๏ธ", "โŒจ๏ธ", "๐Ÿš", "๐Ÿ“Ÿ", "๐Ÿ”ฒ", "โ–ถ๏ธ", "โฌ›"],
13    ),
14    (
15        "Dev & Tools",
16        &["๐Ÿ”ง", "๐Ÿ› ๏ธ", "โš™๏ธ", "๐Ÿ”จ", "๐Ÿงฐ", "๐Ÿ“", "๐Ÿ”ฌ", "๐Ÿงช"],
17    ),
18    (
19        "Files & Data",
20        &["๐Ÿ“", "๐Ÿ“‚", "๐Ÿ“„", "๐Ÿ“Š", "๐Ÿ’พ", "๐Ÿ—„๏ธ", "๐Ÿ“ฆ", "๐Ÿ—ƒ๏ธ"],
21    ),
22    (
23        "Network & Cloud",
24        &["๐ŸŒ", "โ˜๏ธ", "๐Ÿ“ก", "๐Ÿ”—", "๐ŸŒ", "๐Ÿ“ถ", "๐Ÿ›ฐ๏ธ", "๐ŸŒŽ"],
25    ),
26    (
27        "Security",
28        &["๐Ÿ”’", "๐Ÿ”‘", "๐Ÿ›ก๏ธ", "๐Ÿ”", "๐Ÿšจ", "โš ๏ธ", "๐Ÿ”“", "๐Ÿงฑ"],
29    ),
30    (
31        "Status & Alerts",
32        &["โœ…", "โŒ", "โšก", "๐Ÿ””", "๐Ÿ’ก", "๐Ÿš€", "๐ŸŽฏ", "๐Ÿ”ฅ"],
33    ),
34    (
35        "Containers & Infra",
36        &["๐Ÿณ", "๐Ÿง", "๐Ÿ—๏ธ", "๐Ÿ“€", "๐ŸงŠ", "๐Ÿ“‹", "๐Ÿ”„", "๐Ÿ "],
37    ),
38    (
39        "People & Roles",
40        &["๐Ÿ‘ค", "๐Ÿ‘จโ€๐Ÿ’ป", "๐Ÿ‘ฉโ€๐Ÿ’ป", "๐Ÿค–", "๐Ÿ‘ฅ", "๐Ÿง‘โ€๐Ÿ”ง", "๐Ÿง‘โ€๐Ÿซ", "๐Ÿ‘ท"],
41    ),
42    ("Misc", &["๐ŸŽจ", "๐Ÿ“", "๐Ÿท๏ธ", "โญ", "๐Ÿ’Ž", "๐ŸŒˆ", "๐ŸŽฎ", "๐ŸŽต"]),
43];
44
45/// Actions that can be triggered from the profile modal
46#[derive(Debug, Clone, PartialEq)]
47pub enum ProfileModalAction {
48    /// No action
49    None,
50    /// Save changes to profiles and close modal
51    Save,
52    /// Cancel and discard changes
53    Cancel,
54    /// Open a profile immediately (after creation)
55    #[allow(dead_code)]
56    OpenProfile(ProfileId),
57}
58
59/// Modal display mode
60#[derive(Debug, Clone, PartialEq)]
61enum ModalMode {
62    /// Viewing the list of profiles
63    List,
64    /// Editing an existing profile
65    Edit(ProfileId),
66    /// Creating a new profile
67    Create,
68}
69
70/// Profile modal UI state
71pub struct ProfileModalUI {
72    /// Whether the modal is visible
73    pub visible: bool,
74    /// Current display mode
75    mode: ModalMode,
76    /// Working copy of profiles being edited
77    working_profiles: Vec<Profile>,
78    /// ID of profile being edited/created
79    editing_id: Option<ProfileId>,
80
81    // Temporary form fields
82    temp_name: String,
83    temp_working_dir: String,
84    temp_shell: Option<String>,
85    temp_login_shell: Option<bool>,
86    temp_command: String,
87    temp_args: String,
88    temp_tab_name: String,
89    temp_icon: String,
90    // New fields for enhanced profile system (issue #78)
91    temp_tags: String,
92    temp_parent_id: Option<ProfileId>,
93    temp_keyboard_shortcut: String,
94    temp_hostname_patterns: String,
95    temp_tmux_session_patterns: String,
96    temp_directory_patterns: String,
97    temp_badge_text: String,
98    // Badge appearance settings
99    temp_badge_color: Option<[u8; 3]>,
100    temp_badge_color_alpha: Option<f32>,
101    temp_badge_font: String,
102    temp_badge_font_bold: Option<bool>,
103    temp_badge_top_margin: Option<f32>,
104    temp_badge_right_margin: Option<f32>,
105    temp_badge_max_width: Option<f32>,
106    temp_badge_max_height: Option<f32>,
107    /// Whether badge settings section is expanded
108    badge_section_expanded: bool,
109    /// Whether SSH settings section is expanded
110    ssh_section_expanded: bool,
111    // SSH temp fields
112    temp_ssh_host: String,
113    temp_ssh_user: String,
114    temp_ssh_port: String,
115    temp_ssh_identity_file: String,
116    temp_ssh_extra_args: String,
117
118    /// Selected profile in list view
119    selected_id: Option<ProfileId>,
120    /// Whether there are unsaved changes
121    has_changes: bool,
122    /// Validation error message
123    validation_error: Option<String>,
124    /// Profile pending deletion (for confirmation)
125    pending_delete: Option<(ProfileId, String)>,
126}
127
128impl ProfileModalUI {
129    /// Create a new profile modal UI
130    pub fn new() -> Self {
131        Self {
132            visible: false,
133            mode: ModalMode::List,
134            working_profiles: Vec::new(),
135            editing_id: None,
136            temp_name: String::new(),
137            temp_working_dir: String::new(),
138            temp_shell: None,
139            temp_login_shell: None,
140            temp_command: String::new(),
141            temp_args: String::new(),
142            temp_tab_name: String::new(),
143            temp_icon: String::new(),
144            temp_tags: String::new(),
145            temp_parent_id: None,
146            temp_keyboard_shortcut: String::new(),
147            temp_hostname_patterns: String::new(),
148            temp_tmux_session_patterns: String::new(),
149            temp_directory_patterns: String::new(),
150            temp_badge_text: String::new(),
151            temp_badge_color: None,
152            temp_badge_color_alpha: None,
153            temp_badge_font: String::new(),
154            temp_badge_font_bold: None,
155            temp_badge_top_margin: None,
156            temp_badge_right_margin: None,
157            temp_badge_max_width: None,
158            temp_badge_max_height: None,
159            badge_section_expanded: false,
160            ssh_section_expanded: false,
161            temp_ssh_host: String::new(),
162            temp_ssh_user: String::new(),
163            temp_ssh_port: String::new(),
164            temp_ssh_identity_file: String::new(),
165            temp_ssh_extra_args: String::new(),
166            selected_id: None,
167            has_changes: false,
168            validation_error: None,
169            pending_delete: None,
170        }
171    }
172
173    /// Open the modal with current profiles
174    pub fn open(&mut self, manager: &ProfileManager) {
175        self.visible = true;
176        self.mode = ModalMode::List;
177        self.working_profiles = manager.to_vec();
178        self.editing_id = None;
179        self.selected_id = None;
180        self.has_changes = false;
181        self.validation_error = None;
182        self.pending_delete = None;
183        self.clear_form();
184        log::info!(
185            "Profile modal opened with {} profiles",
186            self.working_profiles.len()
187        );
188    }
189
190    /// Close the modal
191    pub fn close(&mut self) {
192        self.visible = false;
193        self.mode = ModalMode::List;
194        self.working_profiles.clear();
195        self.editing_id = None;
196        self.pending_delete = None;
197        self.clear_form();
198    }
199
200    /// Load profiles into the working copy without toggling visibility.
201    ///
202    /// Used by the settings window to populate the inline profile editor
203    /// without opening a modal window.
204    pub fn load_profiles(&mut self, profiles: Vec<Profile>) {
205        self.working_profiles = profiles;
206        self.mode = ModalMode::List;
207        self.editing_id = None;
208        self.selected_id = None;
209        self.has_changes = false;
210        self.validation_error = None;
211        self.pending_delete = None;
212        self.clear_form();
213    }
214
215    /// Get the working profiles (for saving)
216    pub fn get_working_profiles(&self) -> &[Profile] {
217        &self.working_profiles
218    }
219
220    /// Clear form fields
221    fn clear_form(&mut self) {
222        self.temp_name.clear();
223        self.temp_working_dir.clear();
224        self.temp_shell = None;
225        self.temp_login_shell = None;
226        self.temp_command.clear();
227        self.temp_args.clear();
228        self.temp_tab_name.clear();
229        self.temp_icon.clear();
230        self.temp_tags.clear();
231        self.temp_parent_id = None;
232        self.temp_keyboard_shortcut.clear();
233        self.temp_hostname_patterns.clear();
234        self.temp_tmux_session_patterns.clear();
235        self.temp_directory_patterns.clear();
236        self.temp_badge_text.clear();
237        self.temp_badge_color = None;
238        self.temp_badge_color_alpha = None;
239        self.temp_badge_font.clear();
240        self.temp_badge_font_bold = None;
241        self.temp_badge_top_margin = None;
242        self.temp_badge_right_margin = None;
243        self.temp_badge_max_width = None;
244        self.temp_badge_max_height = None;
245        self.temp_ssh_host.clear();
246        self.temp_ssh_user.clear();
247        self.temp_ssh_port.clear();
248        self.temp_ssh_identity_file.clear();
249        self.temp_ssh_extra_args.clear();
250        self.validation_error = None;
251    }
252
253    /// Load a profile into the form
254    fn load_profile_to_form(&mut self, profile: &Profile) {
255        self.temp_name = profile.name.clone();
256        self.temp_working_dir = profile.working_directory.clone().unwrap_or_default();
257        self.temp_shell = profile.shell.clone();
258        self.temp_login_shell = profile.login_shell;
259        self.temp_command = profile.command.clone().unwrap_or_default();
260        self.temp_args = profile
261            .command_args
262            .as_ref()
263            .map(|args| args.join(" "))
264            .unwrap_or_default();
265        self.temp_tab_name = profile.tab_name.clone().unwrap_or_default();
266        self.temp_icon = profile.icon.clone().unwrap_or_default();
267        // New fields
268        self.temp_tags = profile.tags.join(", ");
269        self.temp_parent_id = profile.parent_id;
270        self.temp_keyboard_shortcut = profile.keyboard_shortcut.clone().unwrap_or_default();
271        self.temp_hostname_patterns = profile.hostname_patterns.join(", ");
272        self.temp_tmux_session_patterns = profile.tmux_session_patterns.join(", ");
273        self.temp_directory_patterns = profile.directory_patterns.join(", ");
274        self.temp_badge_text = profile.badge_text.clone().unwrap_or_default();
275        // Badge appearance settings
276        self.temp_badge_color = profile.badge_color;
277        self.temp_badge_color_alpha = profile.badge_color_alpha;
278        self.temp_badge_font = profile.badge_font.clone().unwrap_or_default();
279        self.temp_badge_font_bold = profile.badge_font_bold;
280        self.temp_badge_top_margin = profile.badge_top_margin;
281        self.temp_badge_right_margin = profile.badge_right_margin;
282        self.temp_badge_max_width = profile.badge_max_width;
283        self.temp_badge_max_height = profile.badge_max_height;
284        // SSH fields
285        self.temp_ssh_host = profile.ssh_host.clone().unwrap_or_default();
286        self.temp_ssh_user = profile.ssh_user.clone().unwrap_or_default();
287        self.temp_ssh_port = profile.ssh_port.map(|p| p.to_string()).unwrap_or_default();
288        self.temp_ssh_identity_file = profile.ssh_identity_file.clone().unwrap_or_default();
289        self.temp_ssh_extra_args = profile.ssh_extra_args.clone().unwrap_or_default();
290    }
291
292    /// Create a profile from form fields
293    fn form_to_profile(&self, id: ProfileId, order: usize) -> Profile {
294        let mut profile = Profile::with_id(id, self.temp_name.trim());
295        profile.order = order;
296
297        if !self.temp_working_dir.is_empty() {
298            profile.working_directory = Some(self.temp_working_dir.clone());
299        }
300        profile.shell = self.temp_shell.clone();
301        profile.login_shell = self.temp_login_shell;
302        if !self.temp_command.is_empty() {
303            profile.command = Some(self.temp_command.clone());
304        }
305        if !self.temp_args.is_empty() {
306            // Parse space-separated arguments
307            profile.command_args = Some(
308                self.temp_args
309                    .split_whitespace()
310                    .map(String::from)
311                    .collect(),
312            );
313        }
314        if !self.temp_tab_name.is_empty() {
315            profile.tab_name = Some(self.temp_tab_name.clone());
316        }
317        if !self.temp_icon.is_empty() {
318            profile.icon = Some(self.temp_icon.clone());
319        }
320        // New fields
321        if !self.temp_tags.is_empty() {
322            profile.tags = self
323                .temp_tags
324                .split(',')
325                .map(|s| s.trim().to_string())
326                .filter(|s| !s.is_empty())
327                .collect();
328        }
329        profile.parent_id = self.temp_parent_id;
330        if !self.temp_keyboard_shortcut.is_empty() {
331            profile.keyboard_shortcut = Some(self.temp_keyboard_shortcut.clone());
332        }
333        if !self.temp_hostname_patterns.is_empty() {
334            profile.hostname_patterns = self
335                .temp_hostname_patterns
336                .split(',')
337                .map(|s| s.trim().to_string())
338                .filter(|s| !s.is_empty())
339                .collect();
340        }
341        if !self.temp_tmux_session_patterns.is_empty() {
342            profile.tmux_session_patterns = self
343                .temp_tmux_session_patterns
344                .split(',')
345                .map(|s| s.trim().to_string())
346                .filter(|s| !s.is_empty())
347                .collect();
348        }
349        if !self.temp_directory_patterns.is_empty() {
350            profile.directory_patterns = self
351                .temp_directory_patterns
352                .split(',')
353                .map(|s| s.trim().to_string())
354                .filter(|s| !s.is_empty())
355                .collect();
356        }
357        if !self.temp_badge_text.is_empty() {
358            profile.badge_text = Some(self.temp_badge_text.clone());
359        }
360        // Badge appearance settings
361        profile.badge_color = self.temp_badge_color;
362        profile.badge_color_alpha = self.temp_badge_color_alpha;
363        if !self.temp_badge_font.is_empty() {
364            profile.badge_font = Some(self.temp_badge_font.clone());
365        }
366        profile.badge_font_bold = self.temp_badge_font_bold;
367        profile.badge_top_margin = self.temp_badge_top_margin;
368        profile.badge_right_margin = self.temp_badge_right_margin;
369        profile.badge_max_width = self.temp_badge_max_width;
370        profile.badge_max_height = self.temp_badge_max_height;
371        // SSH fields
372        if !self.temp_ssh_host.is_empty() {
373            profile.ssh_host = Some(self.temp_ssh_host.clone());
374        }
375        if !self.temp_ssh_user.is_empty() {
376            profile.ssh_user = Some(self.temp_ssh_user.clone());
377        }
378        if !self.temp_ssh_port.is_empty() {
379            profile.ssh_port = self.temp_ssh_port.parse().ok();
380        }
381        if !self.temp_ssh_identity_file.is_empty() {
382            profile.ssh_identity_file = Some(self.temp_ssh_identity_file.clone());
383        }
384        if !self.temp_ssh_extra_args.is_empty() {
385            profile.ssh_extra_args = Some(self.temp_ssh_extra_args.clone());
386        }
387
388        profile
389    }
390
391    /// Validate form fields
392    fn validate_form(&self) -> Option<String> {
393        if self.temp_name.trim().is_empty() {
394            return Some("Profile name is required".to_string());
395        }
396        None
397    }
398
399    /// Start editing an existing profile
400    fn start_edit(&mut self, id: ProfileId) {
401        if let Some(profile) = self.working_profiles.iter().find(|p| p.id == id).cloned() {
402            self.load_profile_to_form(&profile);
403            self.editing_id = Some(id);
404            self.mode = ModalMode::Edit(id);
405        }
406    }
407
408    /// Start creating a new profile
409    fn start_create(&mut self) {
410        self.clear_form();
411        self.temp_name = "New Profile".to_string();
412        let new_id = uuid::Uuid::new_v4();
413        self.editing_id = Some(new_id);
414        self.mode = ModalMode::Create;
415    }
416
417    /// Save the current form (either update existing or create new)
418    fn save_form(&mut self) {
419        if let Some(error) = self.validate_form() {
420            self.validation_error = Some(error);
421            return;
422        }
423
424        if let Some(id) = self.editing_id {
425            match &self.mode {
426                ModalMode::Create => {
427                    let order = self.working_profiles.len();
428                    let profile = self.form_to_profile(id, order);
429                    self.working_profiles.push(profile);
430                    log::info!("Created new profile: {}", self.temp_name);
431                }
432                ModalMode::Edit(edit_id) => {
433                    if let Some(existing) =
434                        self.working_profiles.iter().position(|p| p.id == *edit_id)
435                    {
436                        let order = self.working_profiles[existing].order;
437                        let profile = self.form_to_profile(id, order);
438                        self.working_profiles[existing] = profile;
439                        log::info!("Updated profile: {}", self.temp_name);
440                    }
441                }
442                ModalMode::List => {}
443            }
444            self.has_changes = true;
445        }
446
447        self.mode = ModalMode::List;
448        self.editing_id = None;
449        self.clear_form();
450    }
451
452    /// Cancel editing and return to list view
453    fn cancel_edit(&mut self) {
454        self.mode = ModalMode::List;
455        self.editing_id = None;
456        self.clear_form();
457    }
458
459    /// Request deletion of a profile (shows confirmation)
460    fn request_delete(&mut self, id: ProfileId, name: String) {
461        self.pending_delete = Some((id, name));
462    }
463
464    /// Confirm and execute profile deletion
465    fn confirm_delete(&mut self) {
466        if let Some((id, name)) = self.pending_delete.take() {
467            self.working_profiles.retain(|p| p.id != id);
468            self.has_changes = true;
469            if self.selected_id == Some(id) {
470                self.selected_id = None;
471            }
472            log::info!("Deleted profile: {}", name);
473        }
474    }
475
476    /// Cancel pending deletion
477    fn cancel_delete(&mut self) {
478        self.pending_delete = None;
479    }
480
481    /// Move a profile up in the list
482    fn move_up(&mut self, id: ProfileId) {
483        if let Some(pos) = self.working_profiles.iter().position(|p| p.id == id)
484            && pos > 0
485        {
486            self.working_profiles.swap(pos, pos - 1);
487            // Update order values
488            for (i, p) in self.working_profiles.iter_mut().enumerate() {
489                p.order = i;
490            }
491            self.has_changes = true;
492        }
493    }
494
495    /// Move a profile down in the list
496    fn move_down(&mut self, id: ProfileId) {
497        if let Some(pos) = self.working_profiles.iter().position(|p| p.id == id)
498            && pos < self.working_profiles.len() - 1
499        {
500            self.working_profiles.swap(pos, pos + 1);
501            // Update order values
502            for (i, p) in self.working_profiles.iter_mut().enumerate() {
503                p.order = i;
504            }
505            self.has_changes = true;
506        }
507    }
508
509    /// Render the profile list/edit UI inline (no egui::Window wrapper).
510    ///
511    /// Used inside the settings window's Profiles tab to embed the profile
512    /// management UI directly. Returns `ProfileModalAction` to communicate
513    /// save/cancel/open-profile requests to the caller.
514    pub fn show_inline(&mut self, ui: &mut egui::Ui) -> ProfileModalAction {
515        let action = match &self.mode.clone() {
516            ModalMode::List => self.render_list_view(ui),
517            ModalMode::Edit(_) | ModalMode::Create => {
518                self.render_edit_view(ui);
519                ProfileModalAction::None
520            }
521        };
522
523        // Render delete confirmation dialog on top
524        if self.pending_delete.is_some() {
525            self.render_delete_confirmation(ui.ctx());
526        }
527
528        action
529    }
530
531    /// Render the modal and return any action triggered
532    pub fn show(&mut self, ctx: &egui::Context) -> ProfileModalAction {
533        if !self.visible {
534            return ProfileModalAction::None;
535        }
536
537        let mut action = ProfileModalAction::None;
538
539        // Handle Escape key
540        if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
541            match &self.mode {
542                ModalMode::Edit(_) | ModalMode::Create => {
543                    self.cancel_edit();
544                }
545                ModalMode::List => {
546                    self.close();
547                    return ProfileModalAction::Cancel;
548                }
549            }
550        }
551
552        let modal_size = egui::vec2(550.0, 580.0);
553
554        egui::Window::new("Manage Profiles")
555            .collapsible(false)
556            .resizable(false)
557            .order(egui::Order::Foreground)
558            .default_size(modal_size)
559            .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
560            .frame(
561                egui::Frame::window(&ctx.style())
562                    .fill(egui::Color32::from_rgba_unmultiplied(30, 30, 30, 250))
563                    .inner_margin(egui::Margin::same(16)),
564            )
565            .show(ctx, |ui| match &self.mode.clone() {
566                ModalMode::List => {
567                    action = self.render_list_view(ui);
568                }
569                ModalMode::Edit(_) | ModalMode::Create => {
570                    self.render_edit_view(ui);
571                }
572            });
573
574        // Render delete confirmation dialog on top
575        if self.pending_delete.is_some() {
576            self.render_delete_confirmation(ctx);
577        }
578
579        action
580    }
581
582    /// Render delete confirmation dialog
583    fn render_delete_confirmation(&mut self, ctx: &egui::Context) {
584        let (_, profile_name) = self.pending_delete.as_ref().unwrap();
585        let name = profile_name.clone();
586
587        egui::Window::new("Confirm Delete")
588            .collapsible(false)
589            .resizable(false)
590            .order(egui::Order::Foreground)
591            .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
592            .frame(
593                egui::Frame::window(&ctx.style())
594                    .fill(egui::Color32::from_rgba_unmultiplied(40, 40, 40, 255))
595                    .inner_margin(egui::Margin::same(20)),
596            )
597            .show(ctx, |ui| {
598                ui.vertical_centered(|ui| {
599                    ui.label(format!("Delete profile \"{}\"?", name));
600                    ui.add_space(8.0);
601                    ui.label(
602                        egui::RichText::new("This action cannot be undone.")
603                            .small()
604                            .color(egui::Color32::GRAY),
605                    );
606                    ui.add_space(16.0);
607                    ui.horizontal(|ui| {
608                        if ui.button("Delete").clicked() {
609                            self.confirm_delete();
610                        }
611                        if ui.button("Cancel").clicked() {
612                            self.cancel_delete();
613                        }
614                    });
615                });
616            });
617    }
618
619    /// Render the list view
620    pub(crate) fn render_list_view(&mut self, ui: &mut egui::Ui) -> ProfileModalAction {
621        let mut action = ProfileModalAction::None;
622
623        // Header with create button
624        ui.horizontal(|ui| {
625            ui.heading("Profiles");
626            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
627                if ui.button("+ New Profile").clicked() {
628                    self.start_create();
629                }
630            });
631        });
632        ui.separator();
633
634        // Profile list
635        let available_height = ui.available_height() - 50.0; // Reserve space for footer
636        egui::ScrollArea::vertical()
637            .max_height(available_height)
638            .show(ui, |ui| {
639                if self.working_profiles.is_empty() {
640                    ui.vertical_centered(|ui| {
641                        ui.add_space(40.0);
642                        ui.label(
643                            egui::RichText::new("No profiles yet")
644                                .italics()
645                                .color(egui::Color32::GRAY),
646                        );
647                        ui.add_space(10.0);
648                        ui.label("Click '+ New Profile' to create one");
649                    });
650                } else {
651                    for (idx, profile) in self.working_profiles.clone().iter().enumerate() {
652                        let is_selected = self.selected_id == Some(profile.id);
653
654                        // Use push_id with profile.id to ensure stable widget ID for double-click detection
655                        ui.push_id(profile.id, |ui| {
656                            let bg_color = if is_selected {
657                                egui::Color32::from_rgba_unmultiplied(70, 100, 140, 150)
658                            } else {
659                                egui::Color32::TRANSPARENT
660                            };
661
662                            let frame = egui::Frame::NONE
663                                .fill(bg_color)
664                                .inner_margin(egui::Margin::symmetric(8, 4))
665                                .corner_radius(4.0);
666
667                            frame.show(ui, |ui| {
668                                ui.horizontal(|ui| {
669                                    // Reorder buttons
670                                    ui.add_enabled_ui(idx > 0, |ui| {
671                                        if ui.small_button("Up").clicked() {
672                                            self.move_up(profile.id);
673                                        }
674                                    });
675                                    ui.add_enabled_ui(
676                                        idx < self.working_profiles.len() - 1,
677                                        |ui| {
678                                            if ui.small_button("Dn").clicked() {
679                                                self.move_down(profile.id);
680                                            }
681                                        },
682                                    );
683
684                                    // Icon and name
685                                    if let Some(icon) = &profile.icon {
686                                        ui.label(icon);
687                                    }
688                                    let name_response =
689                                        ui.selectable_label(is_selected, &profile.name);
690                                    if name_response.clicked() {
691                                        self.selected_id = Some(profile.id);
692                                    }
693                                    if name_response.double_clicked() {
694                                        self.start_edit(profile.id);
695                                    }
696
697                                    // Dynamic profile indicator
698                                    if profile.source.is_dynamic() {
699                                        ui.label(
700                                            egui::RichText::new("[dynamic]")
701                                                .color(egui::Color32::from_rgb(100, 180, 255))
702                                                .small(),
703                                        );
704                                    }
705
706                                    // Spacer
707                                    let is_dynamic = profile.source.is_dynamic();
708                                    ui.with_layout(
709                                        egui::Layout::right_to_left(egui::Align::Center),
710                                        |ui| {
711                                            // Delete button (disabled for dynamic profiles)
712                                            ui.add_enabled_ui(!is_dynamic, |ui| {
713                                                if ui.small_button("๐Ÿ—‘").clicked() {
714                                                    self.request_delete(
715                                                        profile.id,
716                                                        profile.name.clone(),
717                                                    );
718                                                }
719                                            });
720                                            // Edit/View button
721                                            let edit_label =
722                                                if is_dynamic { "๐Ÿ‘" } else { "โœ" };
723                                            if ui.small_button(edit_label).clicked() {
724                                                self.start_edit(profile.id);
725                                            }
726                                        },
727                                    );
728                                });
729                            });
730                        });
731                    }
732                }
733            });
734
735        // Footer buttons
736        ui.separator();
737        ui.horizontal(|ui| {
738            if ui.button("Save").clicked() {
739                action = ProfileModalAction::Save;
740                // Don't call close() here - the caller needs to get working_profiles first
741                // The caller will close the modal after retrieving the profiles
742                self.visible = false;
743            }
744            if ui.button("Cancel").clicked() {
745                action = ProfileModalAction::Cancel;
746                self.close();
747            }
748
749            if self.has_changes {
750                ui.colored_label(egui::Color32::YELLOW, "* Unsaved changes");
751            }
752        });
753
754        action
755    }
756
757    /// Render the edit/create view
758    pub(crate) fn render_edit_view(&mut self, ui: &mut egui::Ui) {
759        // Check if the profile being edited is a dynamic profile
760        let is_dynamic_profile = self
761            .editing_id
762            .and_then(|id| self.working_profiles.iter().find(|p| p.id == id))
763            .is_some_and(|p| p.source.is_dynamic());
764
765        let title = match &self.mode {
766            ModalMode::Create => "Create Profile",
767            ModalMode::Edit(_) => {
768                if is_dynamic_profile {
769                    "View Profile"
770                } else {
771                    "Edit Profile"
772                }
773            }
774            _ => "Profile",
775        };
776
777        ui.heading(title);
778
779        // Show read-only notice for dynamic profiles
780        if is_dynamic_profile {
781            ui.add_space(4.0);
782            ui.horizontal(|ui| {
783                ui.label(egui::RichText::new("โ„น").color(egui::Color32::from_rgb(100, 180, 255)));
784                ui.colored_label(
785                    egui::Color32::from_rgb(100, 180, 255),
786                    "This profile is managed by a remote source and cannot be edited locally.",
787                );
788            });
789        }
790
791        ui.separator();
792
793        // Form in a scrollable area to handle many fields
794        egui::ScrollArea::vertical()
795            .max_height(ui.available_height() - 60.0)
796            .show(ui, |ui| {
797                // Disable all form fields for dynamic (read-only) profiles
798                if is_dynamic_profile {
799                    ui.disable();
800                }
801
802                egui::Grid::new("profile_form")
803                    .num_columns(2)
804                    .spacing([10.0, 8.0])
805                    .show(ui, |ui| {
806                        // === Basic Settings ===
807                        ui.label("Name:");
808                        ui.text_edit_singleline(&mut self.temp_name);
809                        ui.end_row();
810
811                        ui.label("Icon:");
812                        ui.horizontal(|ui| {
813                            ui.text_edit_singleline(&mut self.temp_icon);
814                            let picker_label = if self.temp_icon.is_empty() {
815                                "๐Ÿ˜€"
816                            } else {
817                                &self.temp_icon
818                            };
819                            let picker_btn = ui.button(picker_label);
820                            egui::Popup::from_toggle_button_response(&picker_btn)
821                                .close_behavior(
822                                    egui::PopupCloseBehavior::CloseOnClickOutside,
823                                )
824                                .show(|ui| {
825                                    ui.set_min_width(280.0);
826                                    egui::ScrollArea::vertical()
827                                        .max_height(300.0)
828                                        .show(ui, |ui| {
829                                            for (category, emojis) in EMOJI_PRESETS {
830                                                ui.label(
831                                                    egui::RichText::new(*category)
832                                                        .small()
833                                                        .strong(),
834                                                );
835                                                ui.horizontal_wrapped(|ui| {
836                                                    for emoji in *emojis {
837                                                        let btn = ui.add_sized(
838                                                            [28.0, 28.0],
839                                                            egui::Button::new(*emoji)
840                                                                .frame(false),
841                                                        );
842                                                        if btn.clicked() {
843                                                            self.temp_icon =
844                                                                emoji.to_string();
845                                                            egui::Popup::close_all(
846                                                                ui.ctx(),
847                                                            );
848                                                        }
849                                                    }
850                                                });
851                                                ui.add_space(2.0);
852                                            }
853                                            ui.add_space(4.0);
854                                            if ui.button("Clear icon").clicked() {
855                                                self.temp_icon.clear();
856                                                egui::Popup::close_all(ui.ctx());
857                                            }
858                                        });
859                                });
860                        });
861                        ui.end_row();
862
863                        ui.label("Working Directory:");
864                        ui.horizontal(|ui| {
865                            ui.text_edit_singleline(&mut self.temp_working_dir);
866                            if ui.small_button("Browse...").clicked()
867                                && let Some(path) = rfd::FileDialog::new().pick_folder()
868                            {
869                                self.temp_working_dir = path.display().to_string();
870                            }
871                        });
872                        ui.end_row();
873
874                        // Shell selection dropdown
875                        ui.label("Shell:");
876                        ui.horizontal(|ui| {
877                            let shells = shell_detection::detected_shells();
878                            let selected_label = self
879                                .temp_shell
880                                .as_ref()
881                                .map(|path| {
882                                    // Find display name for selected shell
883                                    shells
884                                        .iter()
885                                        .find(|s| s.path == *path)
886                                        .map(|s| s.name.clone())
887                                        .unwrap_or_else(|| path.clone())
888                                })
889                                .unwrap_or_else(|| "Default (inherit global)".to_string());
890
891                            egui::ComboBox::from_id_salt("shell_selector")
892                                .selected_text(&selected_label)
893                                .show_ui(ui, |ui| {
894                                    // Default option (inherit global)
895                                    if ui
896                                        .selectable_label(
897                                            self.temp_shell.is_none(),
898                                            "Default (inherit global)",
899                                        )
900                                        .clicked()
901                                    {
902                                        self.temp_shell = None;
903                                    }
904                                    ui.separator();
905                                    // Detected shells
906                                    for shell in shells {
907                                        let is_selected = self
908                                            .temp_shell
909                                            .as_ref()
910                                            .is_some_and(|s| s == &shell.path);
911                                        if ui
912                                            .selectable_label(
913                                                is_selected,
914                                                format!("{} ({})", shell.name, shell.path),
915                                            )
916                                            .clicked()
917                                        {
918                                            self.temp_shell = Some(shell.path.clone());
919                                        }
920                                    }
921                                });
922                        });
923                        ui.end_row();
924
925                        // Login shell toggle
926                        ui.label("Login Shell:");
927                        ui.horizontal(|ui| {
928                            let mut use_custom = self.temp_login_shell.is_some();
929                            if ui.checkbox(&mut use_custom, "").changed() {
930                                if use_custom {
931                                    self.temp_login_shell = Some(true);
932                                } else {
933                                    self.temp_login_shell = None;
934                                }
935                            }
936                            if let Some(ref mut login) = self.temp_login_shell {
937                                ui.checkbox(login, "Use login shell (-l)");
938                            } else {
939                                ui.label(
940                                    egui::RichText::new("(inherit global)")
941                                        .small()
942                                        .color(egui::Color32::GRAY),
943                                );
944                            }
945                        });
946                        ui.end_row();
947
948                        ui.label("Command:");
949                        ui.horizontal(|ui| {
950                            ui.text_edit_singleline(&mut self.temp_command);
951                            ui.label(
952                                egui::RichText::new("(overrides shell)")
953                                    .small()
954                                    .color(egui::Color32::GRAY),
955                            );
956                        });
957                        ui.end_row();
958
959                        ui.label("Arguments:");
960                        ui.horizontal(|ui| {
961                            ui.text_edit_singleline(&mut self.temp_args);
962                            ui.label(
963                                egui::RichText::new("(space-separated)")
964                                    .small()
965                                    .color(egui::Color32::GRAY),
966                            );
967                        });
968                        ui.end_row();
969
970                        ui.label("Tab Name:");
971                        ui.horizontal(|ui| {
972                            ui.text_edit_singleline(&mut self.temp_tab_name);
973                            ui.label(
974                                egui::RichText::new("(optional)")
975                                    .small()
976                                    .color(egui::Color32::GRAY),
977                            );
978                        });
979                        ui.end_row();
980                    });
981
982                // === Enhanced Features Section (issue #78) ===
983                ui.add_space(12.0);
984                ui.separator();
985                ui.label(
986                    egui::RichText::new("Enhanced Features")
987                        .strong()
988                        .color(egui::Color32::LIGHT_BLUE),
989                );
990                ui.add_space(4.0);
991
992                egui::Grid::new("profile_form_enhanced")
993                    .num_columns(2)
994                    .spacing([10.0, 8.0])
995                    .show(ui, |ui| {
996                        // Tags
997                        ui.label("Tags:");
998                        ui.horizontal(|ui| {
999                            ui.text_edit_singleline(&mut self.temp_tags);
1000                            ui.label(
1001                                egui::RichText::new("(comma-separated)")
1002                                    .small()
1003                                    .color(egui::Color32::GRAY),
1004                            );
1005                        });
1006                        ui.end_row();
1007
1008                        // Parent profile (inheritance)
1009                        ui.label("Inherit From:");
1010                        self.render_parent_selector(ui);
1011                        ui.end_row();
1012
1013                        // Keyboard shortcut
1014                        ui.label("Keyboard Shortcut:");
1015                        ui.horizontal(|ui| {
1016                            ui.text_edit_singleline(&mut self.temp_keyboard_shortcut);
1017                            ui.label(
1018                                egui::RichText::new({
1019                                    #[cfg(target_os = "macos")]
1020                                    { "(e.g. Cmd+1)" }
1021                                    #[cfg(not(target_os = "macos"))]
1022                                    { "(e.g. Ctrl+Shift+1)" }
1023                                })
1024                                    .small()
1025                                    .color(egui::Color32::GRAY),
1026                            );
1027                        });
1028                        ui.end_row();
1029
1030                        // Hostname patterns for auto-switching
1031                        ui.label("Auto-Switch Hosts:");
1032                        ui.horizontal(|ui| {
1033                            ui.text_edit_singleline(&mut self.temp_hostname_patterns);
1034                            ui.label(
1035                                egui::RichText::new("(*.example.com)")
1036                                    .small()
1037                                    .color(egui::Color32::GRAY),
1038                            );
1039                        });
1040                        ui.end_row();
1041
1042                        // Tmux session patterns for auto-switching
1043                        ui.label("Auto-Switch Tmux:");
1044                        ui.horizontal(|ui| {
1045                            ui.text_edit_singleline(&mut self.temp_tmux_session_patterns);
1046                            ui.label(
1047                                egui::RichText::new("(work-*, *-dev)")
1048                                    .small()
1049                                    .color(egui::Color32::GRAY),
1050                            );
1051                        });
1052                        ui.end_row();
1053
1054                        // Directory patterns for auto-switching
1055                        ui.label("Auto-Switch Dirs:");
1056                        ui.horizontal(|ui| {
1057                            ui.text_edit_singleline(&mut self.temp_directory_patterns);
1058                            ui.label(
1059                                egui::RichText::new("(~/projects/work-*)")
1060                                    .small()
1061                                    .color(egui::Color32::GRAY),
1062                            );
1063                        });
1064                        ui.end_row();
1065
1066                        // Badge text
1067                        ui.label("Badge Text:");
1068                        ui.horizontal(|ui| {
1069                            ui.text_edit_singleline(&mut self.temp_badge_text);
1070                            ui.label(
1071                                egui::RichText::new("(overrides global)")
1072                                    .small()
1073                                    .color(egui::Color32::GRAY),
1074                            );
1075                        });
1076                        ui.end_row();
1077                    });
1078
1079                // Badge Appearance section (collapsible)
1080                ui.add_space(8.0);
1081                egui::CollapsingHeader::new(
1082                    egui::RichText::new("Badge Appearance")
1083                        .strong()
1084                        .color(egui::Color32::LIGHT_BLUE),
1085                )
1086                .default_open(self.badge_section_expanded)
1087                .show(ui, |ui| {
1088                    self.badge_section_expanded = true;
1089                    egui::Grid::new("profile_form_badge_appearance")
1090                        .num_columns(2)
1091                        .spacing([10.0, 8.0])
1092                        .show(ui, |ui| {
1093                            // Badge color
1094                            ui.label("Color:");
1095                            ui.horizontal(|ui| {
1096                                let mut use_custom = self.temp_badge_color.is_some();
1097                                if ui.checkbox(&mut use_custom, "").changed() {
1098                                    if use_custom {
1099                                        self.temp_badge_color = Some([255, 0, 0]); // Default red
1100                                    } else {
1101                                        self.temp_badge_color = None;
1102                                    }
1103                                }
1104                                if let Some(ref mut color) = self.temp_badge_color {
1105                                    let mut egui_color =
1106                                        egui::Color32::from_rgb(color[0], color[1], color[2]);
1107                                    if egui::color_picker::color_edit_button_srgba(
1108                                        ui,
1109                                        &mut egui_color,
1110                                        egui::color_picker::Alpha::Opaque,
1111                                    )
1112                                    .changed()
1113                                    {
1114                                        *color = [egui_color.r(), egui_color.g(), egui_color.b()];
1115                                    }
1116                                } else {
1117                                    ui.label(
1118                                        egui::RichText::new("(use global)")
1119                                            .small()
1120                                            .color(egui::Color32::GRAY),
1121                                    );
1122                                }
1123                            });
1124                            ui.end_row();
1125
1126                            // Badge alpha/opacity
1127                            ui.label("Opacity:");
1128                            ui.horizontal(|ui| {
1129                                let mut use_custom = self.temp_badge_color_alpha.is_some();
1130                                if ui.checkbox(&mut use_custom, "").changed() {
1131                                    if use_custom {
1132                                        self.temp_badge_color_alpha = Some(0.5);
1133                                    } else {
1134                                        self.temp_badge_color_alpha = None;
1135                                    }
1136                                }
1137                                if let Some(ref mut alpha) = self.temp_badge_color_alpha {
1138                                    ui.add(egui::Slider::new(alpha, 0.0..=1.0).step_by(0.05));
1139                                } else {
1140                                    ui.label(
1141                                        egui::RichText::new("(use global)")
1142                                            .small()
1143                                            .color(egui::Color32::GRAY),
1144                                    );
1145                                }
1146                            });
1147                            ui.end_row();
1148
1149                            // Badge font
1150                            ui.label("Font:");
1151                            ui.horizontal(|ui| {
1152                                ui.text_edit_singleline(&mut self.temp_badge_font);
1153                                ui.label(
1154                                    egui::RichText::new("(blank = global)")
1155                                        .small()
1156                                        .color(egui::Color32::GRAY),
1157                                );
1158                            });
1159                            ui.end_row();
1160
1161                            // Badge font bold
1162                            ui.label("Bold:");
1163                            ui.horizontal(|ui| {
1164                                let mut use_custom = self.temp_badge_font_bold.is_some();
1165                                if ui.checkbox(&mut use_custom, "").changed() {
1166                                    if use_custom {
1167                                        self.temp_badge_font_bold = Some(true);
1168                                    } else {
1169                                        self.temp_badge_font_bold = None;
1170                                    }
1171                                }
1172                                if let Some(ref mut bold) = self.temp_badge_font_bold {
1173                                    ui.checkbox(bold, "Bold text");
1174                                } else {
1175                                    ui.label(
1176                                        egui::RichText::new("(use global)")
1177                                            .small()
1178                                            .color(egui::Color32::GRAY),
1179                                    );
1180                                }
1181                            });
1182                            ui.end_row();
1183
1184                            // Badge top margin
1185                            ui.label("Top Margin:");
1186                            ui.horizontal(|ui| {
1187                                let mut use_custom = self.temp_badge_top_margin.is_some();
1188                                if ui.checkbox(&mut use_custom, "").changed() {
1189                                    if use_custom {
1190                                        self.temp_badge_top_margin = Some(0.0);
1191                                    } else {
1192                                        self.temp_badge_top_margin = None;
1193                                    }
1194                                }
1195                                if let Some(ref mut margin) = self.temp_badge_top_margin {
1196                                    ui.add(egui::DragValue::new(margin).range(0.0..=100.0).suffix(" px"));
1197                                } else {
1198                                    ui.label(
1199                                        egui::RichText::new("(use global)")
1200                                            .small()
1201                                            .color(egui::Color32::GRAY),
1202                                    );
1203                                }
1204                            });
1205                            ui.end_row();
1206
1207                            // Badge right margin
1208                            ui.label("Right Margin:");
1209                            ui.horizontal(|ui| {
1210                                let mut use_custom = self.temp_badge_right_margin.is_some();
1211                                if ui.checkbox(&mut use_custom, "").changed() {
1212                                    if use_custom {
1213                                        self.temp_badge_right_margin = Some(16.0);
1214                                    } else {
1215                                        self.temp_badge_right_margin = None;
1216                                    }
1217                                }
1218                                if let Some(ref mut margin) = self.temp_badge_right_margin {
1219                                    ui.add(egui::DragValue::new(margin).range(0.0..=100.0).suffix(" px"));
1220                                } else {
1221                                    ui.label(
1222                                        egui::RichText::new("(use global)")
1223                                            .small()
1224                                            .color(egui::Color32::GRAY),
1225                                    );
1226                                }
1227                            });
1228                            ui.end_row();
1229
1230                            // Badge max width
1231                            ui.label("Max Width:");
1232                            ui.horizontal(|ui| {
1233                                let mut use_custom = self.temp_badge_max_width.is_some();
1234                                if ui.checkbox(&mut use_custom, "").changed() {
1235                                    if use_custom {
1236                                        self.temp_badge_max_width = Some(0.5);
1237                                    } else {
1238                                        self.temp_badge_max_width = None;
1239                                    }
1240                                }
1241                                if let Some(ref mut width) = self.temp_badge_max_width {
1242                                    ui.add(
1243                                        egui::Slider::new(width, 0.1..=1.0)
1244                                            .step_by(0.05)
1245                                            .custom_formatter(|v, _| format!("{:.0}%", v * 100.0)),
1246                                    );
1247                                } else {
1248                                    ui.label(
1249                                        egui::RichText::new("(use global)")
1250                                            .small()
1251                                            .color(egui::Color32::GRAY),
1252                                    );
1253                                }
1254                            });
1255                            ui.end_row();
1256
1257                            // Badge max height
1258                            ui.label("Max Height:");
1259                            ui.horizontal(|ui| {
1260                                let mut use_custom = self.temp_badge_max_height.is_some();
1261                                if ui.checkbox(&mut use_custom, "").changed() {
1262                                    if use_custom {
1263                                        self.temp_badge_max_height = Some(0.2);
1264                                    } else {
1265                                        self.temp_badge_max_height = None;
1266                                    }
1267                                }
1268                                if let Some(ref mut height) = self.temp_badge_max_height {
1269                                    ui.add(
1270                                        egui::Slider::new(height, 0.05..=0.5)
1271                                            .step_by(0.05)
1272                                            .custom_formatter(|v, _| format!("{:.0}%", v * 100.0)),
1273                                    );
1274                                } else {
1275                                    ui.label(
1276                                        egui::RichText::new("(use global)")
1277                                            .small()
1278                                            .color(egui::Color32::GRAY),
1279                                    );
1280                                }
1281                            });
1282                            ui.end_row();
1283                        });
1284
1285                    ui.add_space(4.0);
1286                    ui.label(
1287                        egui::RichText::new("Check boxes to override global badge settings for this profile.")
1288                            .small()
1289                            .color(egui::Color32::GRAY),
1290                    );
1291                });
1292
1293                // SSH Connection section
1294                ui.add_space(8.0);
1295                egui::CollapsingHeader::new(
1296                    egui::RichText::new("SSH Connection")
1297                        .strong()
1298                        .color(egui::Color32::LIGHT_BLUE),
1299                )
1300                .default_open(self.ssh_section_expanded)
1301                .show(ui, |ui| {
1302                    self.ssh_section_expanded = true;
1303                    ui.horizontal(|ui| {
1304                        ui.label("Host:");
1305                        ui.text_edit_singleline(&mut self.temp_ssh_host);
1306                    });
1307                    ui.horizontal(|ui| {
1308                        ui.label("User:");
1309                        ui.text_edit_singleline(&mut self.temp_ssh_user);
1310                    });
1311                    ui.horizontal(|ui| {
1312                        ui.label("Port:");
1313                        ui.add(egui::TextEdit::singleline(&mut self.temp_ssh_port).desired_width(60.0));
1314                    });
1315                    ui.horizontal(|ui| {
1316                        ui.label("Identity File:");
1317                        ui.text_edit_singleline(&mut self.temp_ssh_identity_file);
1318                    });
1319                    ui.horizontal(|ui| {
1320                        ui.label("Extra Args:");
1321                        ui.text_edit_singleline(&mut self.temp_ssh_extra_args);
1322                    });
1323                    ui.add_space(4.0);
1324                    ui.label(
1325                        egui::RichText::new("When SSH Host is set, opening this profile connects via SSH instead of launching a shell.")
1326                            .weak()
1327                            .size(11.0),
1328                    );
1329                });
1330
1331                // Validation error
1332                if let Some(error) = &self.validation_error {
1333                    ui.add_space(8.0);
1334                    ui.colored_label(egui::Color32::RED, error);
1335                }
1336
1337                // Help text
1338                ui.add_space(16.0);
1339                ui.label(
1340                    egui::RichText::new(
1341                        "Note: Inherited settings from parent profiles are used when this profile's field is empty.",
1342                    )
1343                    .small()
1344                    .color(egui::Color32::GRAY),
1345                );
1346            });
1347
1348        // Footer buttons
1349        ui.add_space(8.0);
1350        ui.separator();
1351        ui.horizontal(|ui| {
1352            if is_dynamic_profile {
1353                // Dynamic profiles are read-only; only show Back button
1354                if ui.button("Back").clicked() {
1355                    self.cancel_edit();
1356                }
1357            } else {
1358                if ui.button("Save Profile").clicked() {
1359                    self.save_form();
1360                }
1361                if ui.button("Cancel").clicked() {
1362                    self.cancel_edit();
1363                }
1364            }
1365        });
1366    }
1367
1368    /// Check if `ancestor_id` appears in the parent chain of `profile_id`
1369    fn has_ancestor(&self, profile_id: ProfileId, ancestor_id: ProfileId) -> bool {
1370        let mut current_id = profile_id;
1371        let mut visited = vec![current_id];
1372        while let Some(parent_id) = self
1373            .working_profiles
1374            .iter()
1375            .find(|p| p.id == current_id)
1376            .and_then(|p| p.parent_id)
1377        {
1378            if parent_id == ancestor_id {
1379                return true;
1380            }
1381            if visited.contains(&parent_id) {
1382                return false;
1383            }
1384            visited.push(parent_id);
1385            current_id = parent_id;
1386        }
1387        false
1388    }
1389
1390    /// Render the parent profile selector dropdown
1391    fn render_parent_selector(&mut self, ui: &mut egui::Ui) {
1392        // Get valid parents (excludes self and profiles that would create cycles)
1393        let current_id = self.editing_id;
1394        let valid_parents: Vec<_> = self
1395            .working_profiles
1396            .iter()
1397            .filter(|p| {
1398                // Cannot select self as parent
1399                if Some(p.id) == current_id {
1400                    return false;
1401                }
1402                // Prevent cycles: reject if this candidate has current profile as ancestor
1403                if let Some(cid) = current_id
1404                    && self.has_ancestor(p.id, cid)
1405                {
1406                    return false;
1407                }
1408                true
1409            })
1410            .map(|p| (p.id, p.display_label()))
1411            .collect();
1412
1413        let selected_label = self
1414            .temp_parent_id
1415            .and_then(|id| self.working_profiles.iter().find(|p| p.id == id))
1416            .map(|p| p.display_label())
1417            .unwrap_or_else(|| "(None)".to_string());
1418
1419        egui::ComboBox::from_id_salt("parent_profile_selector")
1420            .selected_text(&selected_label)
1421            .show_ui(ui, |ui| {
1422                // Option to clear parent
1423                if ui
1424                    .selectable_label(self.temp_parent_id.is_none(), "(None)")
1425                    .clicked()
1426                {
1427                    self.temp_parent_id = None;
1428                }
1429                // List valid parents
1430                for (id, label) in valid_parents {
1431                    if ui
1432                        .selectable_label(self.temp_parent_id == Some(id), &label)
1433                        .clicked()
1434                    {
1435                        self.temp_parent_id = Some(id);
1436                    }
1437                }
1438            });
1439    }
1440}
1441
1442impl Default for ProfileModalUI {
1443    fn default() -> Self {
1444        Self::new()
1445    }
1446}