par-term-settings-ui 0.19.0

Settings UI for par-term terminal emulator
Documentation
//! Snippet edit form — used for both new snippets and editing existing ones.

use super::SettingsUI;
use crate::input_tab::capture_key_combo;
use crate::section::collapsing_section;
use par_term_config::snippets::SnippetConfig;
use std::collections::HashSet;

/// Show the snippet edit form (for both new and existing snippets).
pub(super) fn show_snippet_edit_form(
    ui: &mut egui::Ui,
    settings: &mut SettingsUI,
    changes_this_frame: &mut bool,
    edit_index: Option<usize>,
    collapsed: &mut HashSet<String>,
) {
    ui.separator();

    // Buttons at TOP - always visible first
    ui.horizontal(|ui| {
        if ui.button("Save").clicked() {
            let snippet = build_snippet_from_form(settings, edit_index);

            if let Some(i) = edit_index {
                // Update existing snippet
                settings.config.snippets[i] = snippet;
            } else {
                // Add new snippet
                settings.config.snippets.push(snippet);
            }

            settings.has_changes = true;
            *changes_this_frame = true;
            settings.snippets_tab.editing_snippet_index = None;
            settings.snippets_tab.adding_new_snippet = false;
        }

        if ui.button("Cancel").clicked() {
            settings.snippets_tab.editing_snippet_index = None;
            settings.snippets_tab.adding_new_snippet = false;
        }
    });

    ui.separator();

    ui.scope(|ui| {
        ui.label("Title:");
        if ui
            .text_edit_singleline(&mut settings.snippets_tab.temp_snippet_title)
            .changed()
        {
            *changes_this_frame = true;
        }

        ui.label("ID:");
        ui.label(
            egui::RichText::new(&settings.snippets_tab.temp_snippet_id)
                .monospace()
                .small(),
        );

        ui.label("Content:");
        if ui
            .text_edit_multiline(&mut settings.snippets_tab.temp_snippet_content)
            .changed()
        {
            *changes_this_frame = true;
        }

        ui.horizontal(|ui| {
            if ui
                .checkbox(
                    &mut settings.snippets_tab.temp_snippet_auto_execute,
                    "Auto-execute",
                )
                .changed()
            {
                *changes_this_frame = true;
            }
            ui.label(
                egui::RichText::new("⚡ run immediately")
                    .small()
                    .color(egui::Color32::GRAY),
            );
        });

        ui.label("Keybinding:");
        ui.horizontal(|ui| {
            // Check for recording state
            if settings.snippets_tab.recording_snippet_keybinding {
                // Show recording indicator and capture key combo
                ui.label(egui::RichText::new("🔴 Recording...").color(egui::Color32::RED));
                if let Some(combo) = capture_key_combo(ui) {
                    settings.snippets_tab.snippet_recorded_combo = Some(combo.clone());
                    settings.snippets_tab.temp_snippet_keybinding = combo;
                    settings.snippets_tab.recording_snippet_keybinding = false;
                    *changes_this_frame = true;
                }
            } else {
                // Show text input and record button
                if ui
                    .text_edit_singleline(&mut settings.snippets_tab.temp_snippet_keybinding)
                    .changed()
                {
                    *changes_this_frame = true;
                }

                // Check for conflicts
                if !settings.snippets_tab.temp_snippet_keybinding.is_empty() {
                    let exclude_id = if let Some(i) = edit_index {
                        settings.config.snippets.get(i).map(|s| s.id.as_ref())
                    } else {
                        None
                    };

                    if let Some(conflict) = settings.check_keybinding_conflict(
                        &settings.snippets_tab.temp_snippet_keybinding,
                        exclude_id,
                    ) {
                        ui.label(
                            egui::RichText::new(format!("⚠️ {}", conflict))
                                .color(egui::Color32::from_rgb(255, 180, 0))
                                .small(),
                        );
                    }
                }

                // Record button
                if ui
                    .small_button("🎤")
                    .on_hover_text("Record keybinding")
                    .clicked()
                {
                    settings.snippets_tab.recording_snippet_keybinding = true;
                    settings.snippets_tab.snippet_recorded_combo = None;
                }
            }
        });

        // Show keybinding enabled checkbox if keybinding is set
        if !settings.snippets_tab.temp_snippet_keybinding.is_empty() {
            ui.horizontal(|ui| {
                if ui
                    .checkbox(
                        &mut settings.snippets_tab.temp_snippet_keybinding_enabled,
                        "Enabled",
                    )
                    .changed()
                {
                    *changes_this_frame = true;
                }
                ui.label(
                    egui::RichText::new("(disable without removing)")
                        .small()
                        .color(egui::Color32::GRAY),
                );
            });
        }

        ui.label("Folder:");
        if ui
            .text_edit_singleline(&mut settings.snippets_tab.temp_snippet_folder)
            .changed()
        {
            *changes_this_frame = true;
        }

        ui.label("Description:");
        if ui
            .text_edit_singleline(&mut settings.snippets_tab.temp_snippet_description)
            .changed()
        {
            *changes_this_frame = true;
        }

        // Custom Variables section
        let var_count = settings.snippets_tab.temp_snippet_variables.len();
        let header_text = if var_count > 0 {
            format!("Custom Variables ({})", var_count)
        } else {
            "Custom Variables".to_string()
        };
        ui.add_space(4.0);
        collapsing_section(
            ui,
            &header_text,
            "snippet_custom_variables",
            true,
            collapsed,
            |ui| {
                let mut delete_var_index: Option<usize> = None;

                if !settings.snippets_tab.temp_snippet_variables.is_empty() {
                    egui::Grid::new("snippet_variables_edit_grid")
                        .num_columns(3)
                        .spacing([8.0, 4.0])
                        .show(ui, |ui| {
                            ui.label(egui::RichText::new("Name").small().strong());
                            ui.label(egui::RichText::new("Value").small().strong());
                            ui.label(""); // Delete column header
                            ui.end_row();

                            for i in 0..settings.snippets_tab.temp_snippet_variables.len() {
                                let (ref mut name, ref mut value) =
                                    settings.snippets_tab.temp_snippet_variables[i];

                                let name_response = ui.add(
                                    egui::TextEdit::singleline(name)
                                        .desired_width(120.0)
                                        .hint_text("name"),
                                );
                                if name_response.changed() {
                                    *changes_this_frame = true;
                                }

                                let value_response = ui.add(
                                    egui::TextEdit::singleline(value)
                                        .desired_width(160.0)
                                        .hint_text("value"),
                                );
                                if value_response.changed() {
                                    *changes_this_frame = true;
                                }

                                if ui
                                    .small_button(
                                        egui::RichText::new("✕")
                                            .color(egui::Color32::from_rgb(200, 80, 80)),
                                    )
                                    .on_hover_text("Remove variable")
                                    .clicked()
                                {
                                    delete_var_index = Some(i);
                                }
                                ui.end_row();

                                // Warn on empty name
                                if name.is_empty() {
                                    ui.label(
                                        egui::RichText::new("⚠ Name required")
                                            .small()
                                            .color(egui::Color32::from_rgb(255, 180, 0)),
                                    );
                                    ui.label("");
                                    ui.label("");
                                    ui.end_row();
                                }
                            }
                        });
                }

                if let Some(idx) = delete_var_index {
                    settings.snippets_tab.temp_snippet_variables.remove(idx);
                    *changes_this_frame = true;
                }

                if ui.small_button("+ Add Variable").clicked() {
                    settings
                        .snippets_tab
                        .temp_snippet_variables
                        .push((String::new(), String::new()));
                    *changes_this_frame = true;
                }

                ui.label(
                    egui::RichText::new("Use \\(name) in content to reference custom variables")
                        .small()
                        .color(egui::Color32::GRAY),
                );
            },
        );

        // Show variable hint
        ui.label(
            egui::RichText::new("💡 Variables: \\(date), \\(time), \\(session.path), etc.")
                .small()
                .color(egui::Color32::GRAY),
        );
    });

    ui.separator();
}

/// Build the snippet the form describes. `enabled` has no control in the
/// form, so an edited snippet keeps its value; a new snippet starts enabled.
pub(super) fn build_snippet_from_form(
    settings: &SettingsUI,
    edit_index: Option<usize>,
) -> SnippetConfig {
    SnippetConfig {
        id: settings.snippets_tab.temp_snippet_id.clone(),
        title: settings.snippets_tab.temp_snippet_title.clone(),
        content: settings.snippets_tab.temp_snippet_content.clone(),
        keybinding: if settings.snippets_tab.temp_snippet_keybinding.is_empty() {
            None
        } else {
            Some(settings.snippets_tab.temp_snippet_keybinding.clone())
        },
        keybinding_enabled: settings.snippets_tab.temp_snippet_keybinding_enabled,
        folder: if settings.snippets_tab.temp_snippet_folder.is_empty() {
            None
        } else {
            Some(settings.snippets_tab.temp_snippet_folder.clone())
        },
        // The form has no enabled toggle: keep the list's value.
        enabled: edit_index
            .and_then(|i| settings.config.snippets.get(i))
            .is_none_or(|existing| existing.enabled),
        description: if settings.snippets_tab.temp_snippet_description.is_empty() {
            None
        } else {
            Some(settings.snippets_tab.temp_snippet_description.clone())
        },
        auto_execute: settings.snippets_tab.temp_snippet_auto_execute,
        variables: settings
            .snippets_tab
            .temp_snippet_variables
            .iter()
            .cloned()
            .collect(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use par_term_config::Config;

    #[test]
    fn editing_a_disabled_snippet_keeps_it_disabled() {
        let mut snippet = SnippetConfig::new(
            "s1".to_string(),
            "Greeting".to_string(),
            "hello".to_string(),
        );
        snippet.enabled = false;
        let config = Config {
            snippets: vec![snippet],
            ..Config::default()
        };
        let mut settings = SettingsUI::new_for_tests(config);
        settings.snippets_tab.temp_snippet_id = "s1".to_string();
        settings.snippets_tab.temp_snippet_title = "Greeting (edited)".to_string();

        let saved = build_snippet_from_form(&settings, Some(0));

        assert!(!saved.enabled);
        assert_eq!(saved.title, "Greeting (edited)");
    }

    #[test]
    fn a_new_snippet_starts_enabled() {
        let settings = SettingsUI::new_for_tests(Config::default());
        assert!(build_snippet_from_form(&settings, None).enabled);
    }
}