par_term/
settings_ui.rs

1use crate::config::{BackgroundImageMode, Config, CursorStyle, VsyncMode};
2use crate::themes::Theme;
3use arboard::Clipboard;
4use egui::{Color32, Context, Frame, Window, epaint::Shadow};
5use rfd::FileDialog;
6
7/// Result of shader editor actions
8#[derive(Debug, Clone)]
9pub struct ShaderEditorResult {
10    /// New shader source code to compile and apply
11    pub source: String,
12}
13
14/// Settings UI manager using egui
15pub struct SettingsUI {
16    /// Whether the settings window is currently visible
17    pub visible: bool,
18
19    /// Working copy of config being edited
20    config: Config,
21
22    /// Last opacity value that was forwarded for live updates
23    last_live_opacity: f32,
24
25    /// Whether config has unsaved changes
26    has_changes: bool,
27
28    /// Temp strings for optional fields (for UI editing)
29    temp_font_bold: String,
30    temp_font_italic: String,
31    temp_font_bold_italic: String,
32    temp_font_family: String,
33    temp_font_size: f32,
34    temp_line_spacing: f32,
35    temp_char_spacing: f32,
36    temp_enable_text_shaping: bool,
37    temp_enable_ligatures: bool,
38    temp_enable_kerning: bool,
39    font_pending_changes: bool,
40    temp_custom_shell: String,
41    temp_shell_args: String,
42    temp_working_directory: String,
43    temp_background_image: String,
44    temp_custom_shader: String,
45
46    /// Search query used to filter settings sections
47    search_query: String,
48
49    // Shader editor state
50    /// Whether the shader editor window is visible
51    shader_editor_visible: bool,
52    /// The shader source code being edited
53    shader_editor_source: String,
54    /// Shader compilation error message (if any)
55    shader_editor_error: Option<String>,
56    /// Original source when editor was opened (for cancel)
57    shader_editor_original: String,
58
59    // Shader management state
60    /// List of available shader files in the shaders folder
61    available_shaders: Vec<String>,
62    /// Name for new shader (in create dialog)
63    new_shader_name: String,
64    /// Whether to show the create shader dialog
65    show_create_shader_dialog: bool,
66    /// Whether to show the delete confirmation dialog
67    show_delete_shader_dialog: bool,
68
69    // Shader editor search state
70    /// Search query for shader editor
71    shader_search_query: String,
72    /// Byte positions of search matches
73    shader_search_matches: Vec<usize>,
74    /// Current match index (0-based)
75    shader_search_current: usize,
76    /// Whether search bar is visible
77    shader_search_visible: bool,
78}
79
80impl SettingsUI {
81    /// Create a new settings UI
82    pub fn new(config: Config) -> Self {
83        Self {
84            visible: false,
85            temp_font_bold: config.font_family_bold.clone().unwrap_or_default(),
86            temp_font_italic: config.font_family_italic.clone().unwrap_or_default(),
87            temp_font_bold_italic: config.font_family_bold_italic.clone().unwrap_or_default(),
88            temp_font_family: config.font_family.clone(),
89            temp_font_size: config.font_size,
90            temp_line_spacing: config.line_spacing,
91            temp_char_spacing: config.char_spacing,
92            temp_enable_text_shaping: config.enable_text_shaping,
93            temp_enable_ligatures: config.enable_ligatures,
94            temp_enable_kerning: config.enable_kerning,
95            font_pending_changes: false,
96            temp_custom_shell: config.custom_shell.clone().unwrap_or_default(),
97            temp_shell_args: config
98                .shell_args
99                .as_ref()
100                .map(|args| args.join(" "))
101                .unwrap_or_default(),
102            temp_working_directory: config.working_directory.clone().unwrap_or_default(),
103            temp_background_image: config.background_image.clone().unwrap_or_default(),
104            temp_custom_shader: config.custom_shader.clone().unwrap_or_default(),
105            last_live_opacity: config.window_opacity,
106            config,
107            has_changes: false,
108            search_query: String::new(),
109            shader_editor_visible: false,
110            shader_editor_source: String::new(),
111            shader_editor_error: None,
112            shader_editor_original: String::new(),
113            available_shaders: Self::scan_shaders_folder(),
114            new_shader_name: String::new(),
115            show_create_shader_dialog: false,
116            show_delete_shader_dialog: false,
117            shader_search_query: String::new(),
118            shader_search_matches: Vec::new(),
119            shader_search_current: 0,
120            shader_search_visible: false,
121        }
122    }
123
124    /// Scan the shaders folder and return a list of shader filenames
125    fn scan_shaders_folder() -> Vec<String> {
126        let shaders_dir = crate::config::Config::shaders_dir();
127        let mut shaders = Vec::new();
128
129        // Create the shaders directory if it doesn't exist
130        if !shaders_dir.exists()
131            && let Err(e) = std::fs::create_dir_all(&shaders_dir)
132        {
133            log::warn!("Failed to create shaders directory: {}", e);
134            return shaders;
135        }
136
137        // Read all .glsl files from the shaders directory
138        if let Ok(entries) = std::fs::read_dir(&shaders_dir) {
139            for entry in entries.flatten() {
140                let path = entry.path();
141                if path.is_file()
142                    && let Some(ext) = path.extension()
143                    && (ext == "glsl" || ext == "frag" || ext == "shader")
144                    && let Some(name) = path.file_name()
145                {
146                    shaders.push(name.to_string_lossy().to_string());
147                }
148            }
149        }
150
151        shaders.sort();
152        shaders
153    }
154
155    /// Refresh the list of available shaders
156    pub fn refresh_shaders(&mut self) {
157        self.available_shaders = Self::scan_shaders_folder();
158    }
159
160    /// Set shader compilation error (called from app when shader fails to compile)
161    pub fn set_shader_error(&mut self, error: Option<String>) {
162        self.shader_editor_error = error;
163    }
164
165    /// Clear shader error
166    pub fn clear_shader_error(&mut self) {
167        self.shader_editor_error = None;
168    }
169
170    /// Open the shader editor directly (without opening settings)
171    ///
172    /// Returns true if the editor was opened, false if no shader path is configured
173    pub fn open_shader_editor(&mut self) -> bool {
174        if self.temp_custom_shader.is_empty() {
175            log::warn!("Cannot open shader editor: no shader path configured");
176            return false;
177        }
178
179        // Load shader source from file
180        let shader_path = crate::config::Config::shader_path(&self.temp_custom_shader);
181        match std::fs::read_to_string(&shader_path) {
182            Ok(source) => {
183                self.shader_editor_source = source.clone();
184                self.shader_editor_original = source;
185                self.shader_editor_error = None;
186                self.shader_editor_visible = true;
187                log::info!("Shader editor opened for: {}", shader_path.display());
188                true
189            }
190            Err(e) => {
191                self.shader_editor_error = Some(format!(
192                    "Failed to read shader file '{}': {}",
193                    shader_path.display(),
194                    e
195                ));
196                self.shader_editor_visible = true; // Show editor with error
197                log::error!("Failed to load shader: {}", e);
198                true
199            }
200        }
201    }
202
203    /// Update search matches based on current query
204    fn update_shader_search_matches(&mut self) {
205        self.shader_search_matches.clear();
206        self.shader_search_current = 0;
207
208        if self.shader_search_query.is_empty() {
209            return;
210        }
211
212        let query_lower = self.shader_search_query.to_lowercase();
213        let source_lower = self.shader_editor_source.to_lowercase();
214
215        let mut start = 0;
216        while let Some(pos) = source_lower[start..].find(&query_lower) {
217            self.shader_search_matches.push(start + pos);
218            start += pos + query_lower.len();
219        }
220    }
221
222    /// Move to next search match
223    fn shader_search_next(&mut self) {
224        if !self.shader_search_matches.is_empty() {
225            self.shader_search_current =
226                (self.shader_search_current + 1) % self.shader_search_matches.len();
227        }
228    }
229
230    /// Move to previous search match
231    fn shader_search_previous(&mut self) {
232        if !self.shader_search_matches.is_empty() {
233            if self.shader_search_current == 0 {
234                self.shader_search_current = self.shader_search_matches.len() - 1;
235            } else {
236                self.shader_search_current -= 1;
237            }
238        }
239    }
240
241    /// Get the current match position (byte offset) if any
242    fn shader_search_current_pos(&self) -> Option<usize> {
243        if self.shader_search_matches.is_empty() {
244            None
245        } else {
246            Some(self.shader_search_matches[self.shader_search_current])
247        }
248    }
249
250    /// Check if shader editor is visible
251    pub fn is_shader_editor_visible(&self) -> bool {
252        self.shader_editor_visible
253    }
254
255    fn pick_file_path(&self, title: &str) -> Option<String> {
256        FileDialog::new()
257            .set_title(title)
258            .pick_file()
259            .map(|p| p.display().to_string())
260    }
261
262    fn pick_folder_path(&self, title: &str) -> Option<String> {
263        FileDialog::new()
264            .set_title(title)
265            .pick_folder()
266            .map(|p| p.display().to_string())
267    }
268
269    /// Update the config copy (e.g., when config is reloaded)
270    pub fn update_config(&mut self, config: Config) {
271        if !self.has_changes {
272            self.config = config;
273            self.last_live_opacity = self.config.window_opacity;
274
275            // Refresh staged font values only if there aren't unsaved font edits
276            if !self.font_pending_changes {
277                self.sync_font_temps_from_config();
278            }
279        }
280    }
281
282    fn sync_font_temps_from_config(&mut self) {
283        self.temp_font_family = self.config.font_family.clone();
284        self.temp_font_size = self.config.font_size;
285        self.temp_line_spacing = self.config.line_spacing;
286        self.temp_char_spacing = self.config.char_spacing;
287        self.temp_enable_text_shaping = self.config.enable_text_shaping;
288        self.temp_enable_ligatures = self.config.enable_ligatures;
289        self.temp_enable_kerning = self.config.enable_kerning;
290        self.temp_font_bold = self.config.font_family_bold.clone().unwrap_or_default();
291        self.temp_font_italic = self.config.font_family_italic.clone().unwrap_or_default();
292        self.temp_font_bold_italic = self
293            .config
294            .font_family_bold_italic
295            .clone()
296            .unwrap_or_default();
297        self.font_pending_changes = false;
298    }
299
300    fn apply_font_changes(&mut self) {
301        self.config.font_family = self.temp_font_family.clone();
302        self.config.font_size = self.temp_font_size;
303        self.config.line_spacing = self.temp_line_spacing;
304        self.config.char_spacing = self.temp_char_spacing;
305        self.config.enable_text_shaping = self.temp_enable_text_shaping;
306        self.config.enable_ligatures = self.temp_enable_ligatures;
307        self.config.enable_kerning = self.temp_enable_kerning;
308        self.config.font_family_bold = if self.temp_font_bold.is_empty() {
309            None
310        } else {
311            Some(self.temp_font_bold.clone())
312        };
313        self.config.font_family_italic = if self.temp_font_italic.is_empty() {
314            None
315        } else {
316            Some(self.temp_font_italic.clone())
317        };
318        self.config.font_family_bold_italic = if self.temp_font_bold_italic.is_empty() {
319            None
320        } else {
321            Some(self.temp_font_bold_italic.clone())
322        };
323        self.font_pending_changes = false;
324    }
325
326    /// Toggle settings window visibility
327    pub fn toggle(&mut self) {
328        self.visible = !self.visible;
329    }
330
331    /// Get a reference to the working config (for live sync)
332    pub fn current_config(&self) -> &Config {
333        &self.config
334    }
335
336    /// Show the settings window and return (Option<config_to_save>, Option<config_for_live_update>, Option<ShaderEditorResult>)
337    /// - First Option: Some(config) if save was clicked (persist to disk)
338    /// - Second Option: Some(config) if any changes were made (apply immediately)
339    /// - Third Option: Some(ShaderEditorResult) if shader Apply was clicked
340    pub fn show(
341        &mut self,
342        ctx: &Context,
343    ) -> (Option<Config>, Option<Config>, Option<ShaderEditorResult>) {
344        if !self.visible && !self.shader_editor_visible {
345            return (None, None, None);
346        }
347
348        log::info!("SettingsUI.show() called - visible: true");
349
350        // Handle Escape key to close settings window
351        if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
352            if self.shader_editor_visible {
353                // Close shader editor first if open
354                self.shader_editor_visible = false;
355                self.shader_editor_error = None;
356            } else if self.visible {
357                // Close settings window
358                self.visible = false;
359                return (None, None, None);
360            }
361        }
362
363        // Ensure settings panel is fully opaque regardless of terminal opacity
364        let mut style = (*ctx.style()).clone();
365        let solid_bg = Color32::from_rgba_unmultiplied(24, 24, 24, 255);
366        style.visuals.window_fill = solid_bg;
367        style.visuals.panel_fill = solid_bg;
368        style.visuals.widgets.noninteractive.bg_fill = solid_bg;
369        ctx.set_style(style);
370
371        let mut save_requested = false;
372        let mut discard_requested = false;
373        let mut close_requested = false;
374        let mut open = true;
375        let mut changes_this_frame = false;
376        let mut shader_apply_result: Option<ShaderEditorResult> = None;
377
378        // Only show the main settings window if visible
379        if self.visible {
380            let settings_viewport = ctx.input(|i| i.viewport_rect());
381            Window::new("Settings")
382            .resizable(true)
383            .default_width(650.0)
384            .default_height(700.0)
385            .default_pos(settings_viewport.center())
386            .pivot(egui::Align2::CENTER_CENTER)
387            .open(&mut open)
388            .frame(
389                Frame::window(&ctx.style())
390                    .fill(solid_bg)
391                    .stroke(egui::Stroke::NONE)
392                    .shadow(Shadow {
393                        offset: [0, 0],
394                        blur: 0,
395                        spread: 0,
396                        color: Color32::TRANSPARENT,
397                    }),
398            )
399            .show(ctx, |ui| {
400                // Reserve space for fixed footer buttons
401                let available_height = ui.available_height();
402                let footer_height = 45.0;
403
404                // Scrollable content area (takes remaining space above footer)
405                egui::ScrollArea::vertical()
406                    .max_height(available_height - footer_height)
407                    .show(ui, |ui| {
408                    ui.heading("Terminal Settings");
409                    ui.horizontal(|ui| {
410                        ui.label("Quick search:");
411                        ui.add(
412                            egui::TextEdit::singleline(&mut self.search_query)
413                                .hint_text("Type to filter settings"),
414                        );
415                    });
416                    ui.separator();
417
418                    let query = self.search_query.trim().to_lowercase();
419                    let mut matches_found = false;
420                    let mut section_shown = false;
421                    let insert_section_separator = |ui: &mut egui::Ui, shown: &mut bool| {
422                        if *shown {
423                            ui.separator();
424                        } else {
425                            *shown = true;
426                        }
427                    };
428                    let section_matches = |title: &str, fields: &[&str]| -> bool {
429                        if query.is_empty() {
430                            return true;
431                        }
432
433                        let q = query.as_str();
434                        title.to_lowercase().contains(q)
435                            || fields.iter().any(|f| f.to_lowercase().contains(q))
436                    };
437
438                    // Window & Display
439                    if section_matches(
440                        "Window & Display",
441                        &[
442                            "Title",
443                            "Width",
444                            "Height",
445                            "Padding",
446                            "Opacity",
447                            "Decorations",
448                            "Always on top",
449                            "Max FPS",
450                            "VSync",
451                        ],
452                    ) {
453                        insert_section_separator(ui, &mut section_shown);
454                        matches_found = true;
455
456                        ui.collapsing("Window & Display", |ui| {
457                            ui.horizontal(|ui| {
458                                ui.label("Title:");
459                                if ui
460                                    .text_edit_singleline(&mut self.config.window_title)
461                                    .changed()
462                                {
463                                    self.has_changes = true;
464                                    changes_this_frame = true;
465                                }
466                            });
467
468                            ui.horizontal(|ui| {
469                                ui.label("Width:");
470                                if ui
471                                    .add(egui::Slider::new(
472                                        &mut self.config.window_width,
473                                        400..=3840,
474                                    ))
475                                    .changed()
476                                {
477                                    self.has_changes = true;
478                                    changes_this_frame = true;
479                                }
480                            });
481
482                            ui.horizontal(|ui| {
483                                ui.label("Height:");
484                                if ui
485                                    .add(egui::Slider::new(
486                                        &mut self.config.window_height,
487                                        300..=2160,
488                                    ))
489                                    .changed()
490                                {
491                                    self.has_changes = true;
492                                    changes_this_frame = true;
493                                }
494                            });
495
496                            ui.horizontal(|ui| {
497                                ui.label("Padding:");
498                                if ui
499                                    .add(egui::Slider::new(
500                                        &mut self.config.window_padding,
501                                        0.0..=50.0,
502                                    ))
503                                    .changed()
504                                {
505                                    self.has_changes = true;
506                                    changes_this_frame = true;
507                                }
508                            });
509
510                            ui.horizontal(|ui| {
511                                ui.label("Opacity:");
512                                let response = ui.add(egui::Slider::new(
513                                    &mut self.config.window_opacity,
514                                    0.1..=1.0,
515                                ));
516                                if response.changed() {
517                                    log::info!(
518                                        "Opacity slider changed to: {}",
519                                        self.config.window_opacity
520                                    );
521                                    self.has_changes = true;
522                                    changes_this_frame = true;
523                                }
524                            });
525
526                            if ui
527                                .checkbox(&mut self.config.window_decorations, "Window decorations")
528                                .changed()
529                            {
530                                self.has_changes = true;
531                                changes_this_frame = true;
532                            }
533
534                            if ui
535                                .checkbox(&mut self.config.window_always_on_top, "Always on top")
536                                .changed()
537                            {
538                                self.has_changes = true;
539                                changes_this_frame = true;
540                            }
541
542                            ui.horizontal(|ui| {
543                                ui.label("Max FPS:");
544                                if ui
545                                    .add(egui::Slider::new(&mut self.config.max_fps, 1..=240))
546                                    .changed()
547                                {
548                                    self.has_changes = true;
549                                    changes_this_frame = true;
550                                }
551                            });
552
553                            ui.horizontal(|ui| {
554                                ui.label("VSync Mode:");
555                                let current = match self.config.vsync_mode {
556                                    VsyncMode::Immediate => 0,
557                                    VsyncMode::Mailbox => 1,
558                                    VsyncMode::Fifo => 2,
559                                };
560                                let mut selected = current;
561                                egui::ComboBox::from_id_salt("vsync_mode")
562                                    .selected_text(match current {
563                                        0 => "Immediate (No VSync)",
564                                        1 => "Mailbox (Balanced)",
565                                        2 => "FIFO (VSync)",
566                                        _ => "Unknown",
567                                    })
568                                    .show_ui(ui, |ui| {
569                                        ui.selectable_value(
570                                            &mut selected,
571                                            0,
572                                            "Immediate (No VSync)",
573                                        );
574                                        ui.selectable_value(&mut selected, 1, "Mailbox (Balanced)");
575                                        ui.selectable_value(&mut selected, 2, "FIFO (VSync)");
576                                    });
577                                if selected != current {
578                                    self.config.vsync_mode = match selected {
579                                        0 => VsyncMode::Immediate,
580                                        1 => VsyncMode::Mailbox,
581                                        2 => VsyncMode::Fifo,
582                                        _ => VsyncMode::Immediate,
583                                    };
584                                    self.has_changes = true;
585                                }
586                            });
587                        });
588                    }
589
590                    // Terminal
591                    if section_matches(
592                        "Terminal",
593                        &["Columns", "Rows", "Scrollback", "Exit when shell exits"],
594                    ) {
595                        insert_section_separator(ui, &mut section_shown);
596                        matches_found = true;
597
598                        ui.collapsing("Terminal", |ui| {
599                            ui.horizontal(|ui| {
600                                ui.label("Columns:");
601                                if ui
602                                    .add(egui::Slider::new(&mut self.config.cols, 40..=300))
603                                    .changed()
604                                {
605                                    self.has_changes = true;
606                                    changes_this_frame = true;
607                                }
608                            });
609
610                            ui.horizontal(|ui| {
611                                ui.label("Rows:");
612                                if ui
613                                    .add(egui::Slider::new(&mut self.config.rows, 10..=100))
614                                    .changed()
615                                {
616                                    self.has_changes = true;
617                                    changes_this_frame = true;
618                                }
619                            });
620
621                            ui.horizontal(|ui| {
622                                ui.label("Scrollback lines:");
623                                if ui
624                                    .add(egui::Slider::new(
625                                        &mut self.config.scrollback_lines,
626                                        1000..=100000,
627                                    ))
628                                    .changed()
629                                {
630                                    self.has_changes = true;
631                                    changes_this_frame = true;
632                                }
633                            });
634
635                            if ui
636                                .checkbox(
637                                    &mut self.config.exit_on_shell_exit,
638                                    "Exit when shell exits",
639                                )
640                                .changed()
641                            {
642                                self.has_changes = true;
643                                changes_this_frame = true;
644                            }
645                        });
646                    }
647
648                    // Font Settings
649                    if section_matches(
650                        "Font",
651                        &[
652                            "Family",
653                            "Bold",
654                            "Italic",
655                            "Size",
656                            "Line spacing",
657                            "Char spacing",
658                            "Text shaping",
659                            "Ligatures",
660                            "Kerning",
661                        ],
662                    ) {
663                        insert_section_separator(ui, &mut section_shown);
664                        matches_found = true;
665
666                        ui.collapsing("Font", |ui| {
667                            ui.horizontal(|ui| {
668                                ui.label("Family (regular):");
669                                if ui
670                                    .text_edit_singleline(&mut self.temp_font_family)
671                                    .changed()
672                                {
673                                    self.font_pending_changes = true;
674                                }
675                            });
676
677                            ui.horizontal(|ui| {
678                                ui.label("Bold font (optional):");
679                                if ui.text_edit_singleline(&mut self.temp_font_bold).changed() {
680                                    self.font_pending_changes = true;
681                                }
682                            });
683
684                            ui.horizontal(|ui| {
685                                ui.label("Italic font (optional):");
686                                if ui
687                                    .text_edit_singleline(&mut self.temp_font_italic)
688                                    .changed()
689                                {
690                                    self.font_pending_changes = true;
691                                }
692                            });
693
694                            ui.horizontal(|ui| {
695                                ui.label("Bold-Italic font (optional):");
696                                if ui
697                                    .text_edit_singleline(&mut self.temp_font_bold_italic)
698                                    .changed()
699                                {
700                                    self.font_pending_changes = true;
701                                }
702                            });
703
704                            ui.horizontal(|ui| {
705                                ui.label("Size:");
706                                if ui
707                                    .add(egui::Slider::new(&mut self.temp_font_size, 6.0..=48.0))
708                                    .changed()
709                                {
710                                    self.font_pending_changes = true;
711                                }
712                            });
713
714                            ui.horizontal(|ui| {
715                                ui.label("Line spacing:");
716                                if ui
717                                    .add(egui::Slider::new(&mut self.temp_line_spacing, 0.8..=2.0))
718                                    .changed()
719                                {
720                                    self.font_pending_changes = true;
721                                }
722                            });
723
724                            ui.horizontal(|ui| {
725                                ui.label("Char spacing:");
726                                if ui
727                                    .add(egui::Slider::new(&mut self.temp_char_spacing, 0.5..=1.0))
728                                    .changed()
729                                {
730                                    self.font_pending_changes = true;
731                                }
732                            });
733
734                            if ui
735                                .checkbox(&mut self.temp_enable_text_shaping, "Enable text shaping")
736                                .changed()
737                            {
738                                self.font_pending_changes = true;
739                            }
740
741                            if ui
742                                .checkbox(&mut self.temp_enable_ligatures, "Enable ligatures")
743                                .changed()
744                            {
745                                self.font_pending_changes = true;
746                            }
747
748                            if ui
749                                .checkbox(&mut self.temp_enable_kerning, "Enable kerning")
750                                .changed()
751                            {
752                                self.font_pending_changes = true;
753                            }
754
755                            ui.horizontal(|ui| {
756                                if ui.button("Apply font changes").clicked() {
757                                    self.apply_font_changes();
758                                    self.has_changes = true;
759                                    changes_this_frame = true;
760                                }
761                                if self.font_pending_changes {
762                                    ui.colored_label(egui::Color32::YELLOW, "(pending)");
763                                }
764                            });
765                        });
766                    }
767
768                    // Theme & Colors
769                    if section_matches("Theme & Colors", &["Theme"]) {
770                        insert_section_separator(ui, &mut section_shown);
771                        matches_found = true;
772
773                        ui.collapsing("Theme & Colors", |ui| {
774                            let available = Theme::available_themes();
775                            let mut selected = self.config.theme.clone();
776
777                            ui.horizontal(|ui| {
778                                ui.label("Theme:");
779                                egui::ComboBox::from_id_salt("theme_select")
780                                    .width(220.0)
781                                    .selected_text(selected.clone())
782                                    .show_ui(ui, |ui| {
783                                        for theme in &available {
784                                            ui.selectable_value(
785                                                &mut selected,
786                                                theme.to_string(),
787                                                *theme,
788                                            );
789                                        }
790                                    });
791                            });
792
793                            if selected != self.config.theme {
794                                self.config.theme = selected;
795                                self.has_changes = true;
796                                changes_this_frame = true;
797                            }
798                        });
799                    }
800
801                    // Background & Effects
802                    if section_matches(
803                        "Background & Effects",
804                        &[
805                            "Background image",
806                            "Enable background image",
807                            "Shader",
808                            "Enable shader",
809                            "Opacity",
810                            "Animation",
811                            "Mode",
812                            "Text opacity",
813                        ],
814                    ) {
815                        insert_section_separator(ui, &mut section_shown);
816                        matches_found = true;
817
818                        ui.collapsing("Background & Effects", |ui| {
819                            ui.horizontal(|ui| {
820                                ui.label("Background image path:");
821                                if ui
822                                    .text_edit_singleline(&mut self.temp_background_image)
823                                    .changed()
824                                {
825                                    self.config.background_image =
826                                        if self.temp_background_image.is_empty() {
827                                            None
828                                        } else {
829                                            Some(self.temp_background_image.clone())
830                                        };
831                                    self.has_changes = true;
832                                }
833
834                                if ui.button("Browse…").clicked()
835                                    && let Some(path) =
836                                        self.pick_file_path("Select background image")
837                                {
838                                    self.temp_background_image = path.clone();
839                                    self.config.background_image = Some(path);
840                                    self.has_changes = true;
841                                }
842                            });
843
844                            if ui
845                                .checkbox(
846                                    &mut self.config.background_image_enabled,
847                                    "Enable background image",
848                                )
849                                .changed()
850                            {
851                                self.has_changes = true;
852                                changes_this_frame = true;
853                            }
854
855                            ui.horizontal(|ui| {
856                                ui.label("Background image mode:");
857                                let current = match self.config.background_image_mode {
858                                    BackgroundImageMode::Fit => 0,
859                                    BackgroundImageMode::Fill => 1,
860                                    BackgroundImageMode::Stretch => 2,
861                                    BackgroundImageMode::Tile => 3,
862                                    BackgroundImageMode::Center => 4,
863                                };
864                                let mut selected = current;
865                                egui::ComboBox::from_id_salt("bg_mode")
866                                    .selected_text(match current {
867                                        0 => "Fit",
868                                        1 => "Fill",
869                                        2 => "Stretch",
870                                        3 => "Tile",
871                                        4 => "Center",
872                                        _ => "Unknown",
873                                    })
874                                    .show_ui(ui, |ui| {
875                                        ui.selectable_value(&mut selected, 0, "Fit");
876                                        ui.selectable_value(&mut selected, 1, "Fill");
877                                        ui.selectable_value(&mut selected, 2, "Stretch");
878                                        ui.selectable_value(&mut selected, 3, "Tile");
879                                        ui.selectable_value(&mut selected, 4, "Center");
880                                    });
881                                if selected != current {
882                                    self.config.background_image_mode = match selected {
883                                        0 => BackgroundImageMode::Fit,
884                                        1 => BackgroundImageMode::Fill,
885                                        2 => BackgroundImageMode::Stretch,
886                                        3 => BackgroundImageMode::Tile,
887                                        4 => BackgroundImageMode::Center,
888                                        _ => BackgroundImageMode::Stretch,
889                                    };
890                                    self.has_changes = true;
891                                }
892                            });
893
894                            ui.horizontal(|ui| {
895                                ui.label("Background image opacity:");
896                                if ui
897                                    .add(egui::Slider::new(
898                                        &mut self.config.background_image_opacity,
899                                        0.0..=1.0,
900                                    ))
901                                    .changed()
902                                {
903                                    self.has_changes = true;
904                                    changes_this_frame = true;
905                                }
906                            });
907
908                            // Shader selection dropdown
909                            ui.horizontal(|ui| {
910                                ui.label("Shader:");
911                                let selected_text = if self.temp_custom_shader.is_empty() {
912                                    "(none)".to_string()
913                                } else {
914                                    self.temp_custom_shader.clone()
915                                };
916
917                                let mut shader_changed = false;
918                                egui::ComboBox::from_id_salt("shader_select")
919                                    .selected_text(&selected_text)
920                                    .width(200.0)
921                                    .show_ui(ui, |ui| {
922                                        // Option to select none
923                                        if ui.selectable_label(self.temp_custom_shader.is_empty(), "(none)").clicked() {
924                                            self.temp_custom_shader.clear();
925                                            self.config.custom_shader = None;
926                                            shader_changed = true;
927                                        }
928
929                                        // List available shaders
930                                        for shader in &self.available_shaders.clone() {
931                                            let is_selected = self.temp_custom_shader == *shader;
932                                            if ui.selectable_label(is_selected, shader).clicked() {
933                                                self.temp_custom_shader = shader.clone();
934                                                self.config.custom_shader = Some(shader.clone());
935                                                shader_changed = true;
936                                            }
937                                        }
938                                    });
939
940                                if shader_changed {
941                                    self.has_changes = true;
942                                    changes_this_frame = true;
943                                }
944
945                                // Refresh button
946                                if ui.button("↻").on_hover_text("Refresh shader list").clicked() {
947                                    self.refresh_shaders();
948                                }
949                            });
950
951                            // Create and Delete buttons
952                            ui.horizontal(|ui| {
953                                if ui.button("Create New...").clicked() {
954                                    self.new_shader_name.clear();
955                                    self.show_create_shader_dialog = true;
956                                }
957
958                                let has_shader = !self.temp_custom_shader.is_empty();
959                                if ui.add_enabled(has_shader, egui::Button::new("Delete")).clicked() {
960                                    self.show_delete_shader_dialog = true;
961                                }
962
963                                if ui.button("Browse...").on_hover_text("Browse for external shader file").clicked()
964                                    && let Some(path) = self.pick_file_path("Select shader file")
965                                {
966                                    self.temp_custom_shader = path.clone();
967                                    self.config.custom_shader = Some(path);
968                                    self.has_changes = true;
969                                    changes_this_frame = true;
970                                }
971                            });
972
973                            // Show shader compilation error if any
974                            if let Some(error) = &self.shader_editor_error {
975                                let shader_path = crate::config::Config::shader_path(&self.temp_custom_shader);
976                                let full_error = format!("File: {}\n\n{}", shader_path.display(), error);
977                                let error_display = error.clone();
978
979                                ui.add_space(4.0);
980                                egui::Frame::default()
981                                    .fill(Color32::from_rgb(80, 20, 20))
982                                    .inner_margin(8.0)
983                                    .outer_margin(0.0)
984                                    .corner_radius(4.0)
985                                    .show(ui, |ui| {
986                                        ui.horizontal(|ui| {
987                                            ui.colored_label(Color32::from_rgb(255, 100, 100), "⚠ Shader Error");
988                                            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
989                                                if ui.small_button("Copy").clicked()
990                                                    && let Ok(mut clipboard) = Clipboard::new()
991                                                {
992                                                    let _ = clipboard.set_text(full_error.clone());
993                                                }
994                                            });
995                                        });
996                                        // Show shader path on its own line
997                                        ui.label(format!("File: {}", shader_path.display()));
998                                        ui.separator();
999                                        // Show error details with word wrap
1000                                        ui.add(
1001                                            egui::TextEdit::multiline(&mut error_display.as_str())
1002                                                .font(egui::TextStyle::Monospace)
1003                                                .desired_width(f32::INFINITY)
1004                                                .desired_rows(3)
1005                                                .interactive(false)
1006                                        );
1007                                    });
1008                                ui.add_space(4.0);
1009                            }
1010
1011                            if ui
1012                                .checkbox(
1013                                    &mut self.config.custom_shader_enabled,
1014                                    "Enable custom shader",
1015                                )
1016                                .changed()
1017                            {
1018                                self.has_changes = true;
1019                                changes_this_frame = true;
1020                            }
1021
1022                            if ui
1023                                .checkbox(
1024                                    &mut self.config.custom_shader_animation,
1025                                    "Enable shader animation",
1026                                )
1027                                .changed()
1028                            {
1029                                self.has_changes = true;
1030                                changes_this_frame = true;
1031                            }
1032
1033                            ui.horizontal(|ui| {
1034                                ui.label("Animation speed:");
1035                                if ui
1036                                    .add(egui::Slider::new(
1037                                        &mut self.config.custom_shader_animation_speed,
1038                                        0.0..=5.0,
1039                                    ))
1040                                    .changed()
1041                                {
1042                                    self.has_changes = true;
1043                                    changes_this_frame = true;
1044                                }
1045                            });
1046
1047                            ui.horizontal(|ui| {
1048                                ui.label("Shader text opacity:");
1049                                if ui
1050                                    .add(egui::Slider::new(
1051                                        &mut self.config.custom_shader_text_opacity,
1052                                        0.0..=1.0,
1053                                    ))
1054                                    .changed()
1055                                {
1056                                    self.has_changes = true;
1057                                    changes_this_frame = true;
1058                                }
1059                            });
1060
1061                            if ui
1062                                .checkbox(
1063                                    &mut self.config.custom_shader_full_content,
1064                                    "Full content mode",
1065                                )
1066                                .on_hover_text("When enabled, shader receives and can manipulate the full terminal content (text + background). When disabled, shader only provides background and text is composited cleanly on top.")
1067                                .changed()
1068                            {
1069                                self.has_changes = true;
1070                                changes_this_frame = true;
1071                            }
1072
1073                            ui.separator();
1074
1075                            // Edit Shader button - only enabled when a shader path is set
1076                            let has_shader_path = !self.temp_custom_shader.is_empty();
1077                            ui.horizontal(|ui| {
1078                                let edit_button = ui.add_enabled(
1079                                    has_shader_path,
1080                                    egui::Button::new("Edit Shader..."),
1081                                );
1082                                if edit_button.clicked() {
1083                                    // Load shader source from file
1084                                    let shader_path = crate::config::Config::shader_path(&self.temp_custom_shader);
1085                                    match std::fs::read_to_string(&shader_path) {
1086                                        Ok(source) => {
1087                                            self.shader_editor_source = source.clone();
1088                                            self.shader_editor_original = source;
1089                                            self.shader_editor_error = None;
1090                                            self.shader_editor_visible = true;
1091                                        }
1092                                        Err(e) => {
1093                                            self.shader_editor_error = Some(format!(
1094                                                "Failed to read shader file '{}': {}",
1095                                                shader_path.display(),
1096                                                e
1097                                            ));
1098                                        }
1099                                    }
1100                                }
1101                                if !has_shader_path {
1102                                    ui.label("(set shader path first)");
1103                                }
1104                            });
1105                        });
1106                    }
1107
1108                    // Cursor
1109                    if section_matches("Cursor", &["Style", "Blink", "Blink interval"]) {
1110                        insert_section_separator(ui, &mut section_shown);
1111                        matches_found = true;
1112
1113                        ui.collapsing("Cursor", |ui| {
1114                            ui.horizontal(|ui| {
1115                                ui.label("Style:");
1116                                let current = match self.config.cursor_style {
1117                                    CursorStyle::Block => 0,
1118                                    CursorStyle::Beam => 1,
1119                                    CursorStyle::Underline => 2,
1120                                };
1121                                let mut selected = current;
1122                                egui::ComboBox::from_id_salt("cursor_style")
1123                                    .selected_text(match current {
1124                                        0 => "Block",
1125                                        1 => "Beam",
1126                                        2 => "Underline",
1127                                        _ => "Unknown",
1128                                    })
1129                                    .show_ui(ui, |ui| {
1130                                        ui.selectable_value(&mut selected, 0, "Block");
1131                                        ui.selectable_value(&mut selected, 1, "Beam");
1132                                        ui.selectable_value(&mut selected, 2, "Underline");
1133                                    });
1134                                if selected != current {
1135                                    self.config.cursor_style = match selected {
1136                                        0 => CursorStyle::Block,
1137                                        1 => CursorStyle::Beam,
1138                                        2 => CursorStyle::Underline,
1139                                        _ => CursorStyle::Block,
1140                                    };
1141                                    self.has_changes = true;
1142                                }
1143                            });
1144
1145                            if ui
1146                                .checkbox(&mut self.config.cursor_blink, "Cursor blink")
1147                                .changed()
1148                            {
1149                                self.has_changes = true;
1150                                changes_this_frame = true;
1151                            }
1152
1153                            ui.horizontal(|ui| {
1154                                ui.label("Blink interval (ms):");
1155                                if ui
1156                                    .add(egui::Slider::new(
1157                                        &mut self.config.cursor_blink_interval,
1158                                        100..=2000,
1159                                    ))
1160                                    .changed()
1161                                {
1162                                    self.has_changes = true;
1163                                    changes_this_frame = true;
1164                                }
1165                            });
1166                        });
1167                    }
1168
1169                    // Selection & Clipboard
1170                    if section_matches(
1171                        "Selection & Clipboard",
1172                        &[
1173                            "Auto-copy",
1174                            "Trailing newline",
1175                            "Middle-click",
1176                            "Max clipboard",
1177                        ],
1178                    ) {
1179                        insert_section_separator(ui, &mut section_shown);
1180                        matches_found = true;
1181
1182                        ui.collapsing("Selection & Clipboard", |ui| {
1183                            if ui
1184                                .checkbox(
1185                                    &mut self.config.auto_copy_selection,
1186                                    "Auto-copy selection",
1187                                )
1188                                .changed()
1189                            {
1190                                self.has_changes = true;
1191                                changes_this_frame = true;
1192                            }
1193
1194                            if ui
1195                                .checkbox(
1196                                    &mut self.config.copy_trailing_newline,
1197                                    "Include trailing newline when copying",
1198                                )
1199                                .changed()
1200                            {
1201                                self.has_changes = true;
1202                                changes_this_frame = true;
1203                            }
1204
1205                            if ui
1206                                .checkbox(&mut self.config.middle_click_paste, "Middle-click paste")
1207                                .changed()
1208                            {
1209                                self.has_changes = true;
1210                                changes_this_frame = true;
1211                            }
1212
1213                            ui.horizontal(|ui| {
1214                                ui.label("Max clipboard sync events:");
1215                                if ui
1216                                    .add(egui::Slider::new(
1217                                        &mut self.config.clipboard_max_sync_events,
1218                                        8..=256,
1219                                    ))
1220                                    .changed()
1221                                {
1222                                    self.has_changes = true;
1223                                    changes_this_frame = true;
1224                                }
1225                            });
1226
1227                            ui.horizontal(|ui| {
1228                                ui.label("Max clipboard event bytes:");
1229                                if ui
1230                                    .add(egui::Slider::new(
1231                                        &mut self.config.clipboard_max_event_bytes,
1232                                        512..=16384,
1233                                    ))
1234                                    .changed()
1235                                {
1236                                    self.has_changes = true;
1237                                    changes_this_frame = true;
1238                                }
1239                            });
1240                        });
1241                    }
1242
1243                    // Mouse Behavior
1244                    if section_matches(
1245                        "Mouse Behavior",
1246                        &["Scroll speed", "Double-click", "Triple-click"],
1247                    ) {
1248                        insert_section_separator(ui, &mut section_shown);
1249                        matches_found = true;
1250
1251                        ui.collapsing("Mouse Behavior", |ui| {
1252                            ui.horizontal(|ui| {
1253                                ui.label("Scroll speed:");
1254                                if ui
1255                                    .add(egui::Slider::new(
1256                                        &mut self.config.mouse_scroll_speed,
1257                                        0.1..=10.0,
1258                                    ))
1259                                    .changed()
1260                                {
1261                                    self.has_changes = true;
1262                                    changes_this_frame = true;
1263                                }
1264                            });
1265
1266                            ui.horizontal(|ui| {
1267                                ui.label("Double-click threshold (ms):");
1268                                if ui
1269                                    .add(egui::Slider::new(
1270                                        &mut self.config.mouse_double_click_threshold,
1271                                        100..=1000,
1272                                    ))
1273                                    .changed()
1274                                {
1275                                    self.has_changes = true;
1276                                    changes_this_frame = true;
1277                                }
1278                            });
1279
1280                            ui.horizontal(|ui| {
1281                                ui.label("Triple-click threshold (ms):");
1282                                if ui
1283                                    .add(egui::Slider::new(
1284                                        &mut self.config.mouse_triple_click_threshold,
1285                                        100..=1000,
1286                                    ))
1287                                    .changed()
1288                                {
1289                                    self.has_changes = true;
1290                                    changes_this_frame = true;
1291                                }
1292                            });
1293                        });
1294                    }
1295
1296                    // Scrollbar
1297                    if section_matches(
1298                        "Scrollbar",
1299                        &[
1300                            "Width",
1301                            "Autohide",
1302                            "Position",
1303                            "Thumb color",
1304                            "Track color",
1305                        ],
1306                    ) {
1307                        insert_section_separator(ui, &mut section_shown);
1308                        matches_found = true;
1309
1310                        ui.collapsing("Scrollbar", |ui| {
1311                            ui.horizontal(|ui| {
1312                                ui.label("Width:");
1313                                if ui
1314                                    .add(egui::Slider::new(
1315                                        &mut self.config.scrollbar_width,
1316                                        4.0..=50.0,
1317                                    ))
1318                                    .changed()
1319                                {
1320                                    self.has_changes = true;
1321                                    changes_this_frame = true;
1322                                }
1323                            });
1324
1325                            ui.horizontal(|ui| {
1326                                ui.label("Autohide delay (ms, 0=never):");
1327                                if ui
1328                                    .add(egui::Slider::new(
1329                                        &mut self.config.scrollbar_autohide_delay,
1330                                        0..=5000,
1331                                    ))
1332                                    .changed()
1333                                {
1334                                    self.has_changes = true;
1335                                    changes_this_frame = true;
1336                                }
1337                            });
1338
1339                            ui.horizontal(|ui| {
1340                                ui.label("Position:");
1341                                ui.label("Right (only)");
1342                            });
1343
1344                            ui.horizontal(|ui| {
1345                                ui.label("Thumb color:");
1346                                let mut thumb = egui::Color32::from_rgba_unmultiplied(
1347                                    (self.config.scrollbar_thumb_color[0] * 255.0) as u8,
1348                                    (self.config.scrollbar_thumb_color[1] * 255.0) as u8,
1349                                    (self.config.scrollbar_thumb_color[2] * 255.0) as u8,
1350                                    (self.config.scrollbar_thumb_color[3] * 255.0) as u8,
1351                                );
1352                                if egui::color_picker::color_edit_button_srgba(
1353                                    ui,
1354                                    &mut thumb,
1355                                    egui::color_picker::Alpha::Opaque,
1356                                )
1357                                .changed()
1358                                {
1359                                    self.config.scrollbar_thumb_color = [
1360                                        thumb.r() as f32 / 255.0,
1361                                        thumb.g() as f32 / 255.0,
1362                                        thumb.b() as f32 / 255.0,
1363                                        thumb.a() as f32 / 255.0,
1364                                    ];
1365                                    self.has_changes = true;
1366                                    changes_this_frame = true;
1367                                }
1368                            });
1369
1370                            ui.horizontal(|ui| {
1371                                ui.label("Track color:");
1372                                let mut track = egui::Color32::from_rgba_unmultiplied(
1373                                    (self.config.scrollbar_track_color[0] * 255.0) as u8,
1374                                    (self.config.scrollbar_track_color[1] * 255.0) as u8,
1375                                    (self.config.scrollbar_track_color[2] * 255.0) as u8,
1376                                    (self.config.scrollbar_track_color[3] * 255.0) as u8,
1377                                );
1378                                if egui::color_picker::color_edit_button_srgba(
1379                                    ui,
1380                                    &mut track,
1381                                    egui::color_picker::Alpha::Opaque,
1382                                )
1383                                .changed()
1384                                {
1385                                    self.config.scrollbar_track_color = [
1386                                        track.r() as f32 / 255.0,
1387                                        track.g() as f32 / 255.0,
1388                                        track.b() as f32 / 255.0,
1389                                        track.a() as f32 / 255.0,
1390                                    ];
1391                                    self.has_changes = true;
1392                                    changes_this_frame = true;
1393                                }
1394                            });
1395                        });
1396                    }
1397
1398                    // Bell & Notifications
1399                    if section_matches(
1400                        "Bell & Notifications",
1401                        &[
1402                            "Visual bell",
1403                            "Audio bell",
1404                            "Desktop notifications",
1405                            "Activity",
1406                            "Silence",
1407                            "Notification buffer",
1408                        ],
1409                    ) {
1410                        insert_section_separator(ui, &mut section_shown);
1411                        matches_found = true;
1412
1413                        ui.collapsing("Bell & Notifications", |ui| {
1414                            ui.label("Bell Settings:");
1415                            if ui
1416                                .checkbox(&mut self.config.notification_bell_visual, "Visual bell")
1417                                .changed()
1418                            {
1419                                self.has_changes = true;
1420                                changes_this_frame = true;
1421                            }
1422
1423                            ui.horizontal(|ui| {
1424                                ui.label("Audio bell volume (0=off):");
1425                                if ui
1426                                    .add(egui::Slider::new(
1427                                        &mut self.config.notification_bell_sound,
1428                                        0..=100,
1429                                    ))
1430                                    .changed()
1431                                {
1432                                    self.has_changes = true;
1433                                    changes_this_frame = true;
1434                                }
1435                            });
1436
1437                            if ui
1438                                .checkbox(
1439                                    &mut self.config.notification_bell_desktop,
1440                                    "Desktop notifications for bell",
1441                                )
1442                                .changed()
1443                            {
1444                                self.has_changes = true;
1445                                changes_this_frame = true;
1446                            }
1447
1448                            ui.separator();
1449                            ui.label("Activity Notifications:");
1450                            if ui
1451                                .checkbox(
1452                                    &mut self.config.notification_activity_enabled,
1453                                    "Notify on activity after inactivity",
1454                                )
1455                                .changed()
1456                            {
1457                                self.has_changes = true;
1458                                changes_this_frame = true;
1459                            }
1460
1461                            ui.horizontal(|ui| {
1462                                ui.label("Activity threshold (seconds):");
1463                                if ui
1464                                    .add(egui::Slider::new(
1465                                        &mut self.config.notification_activity_threshold,
1466                                        1..=300,
1467                                    ))
1468                                    .changed()
1469                                {
1470                                    self.has_changes = true;
1471                                    changes_this_frame = true;
1472                                }
1473                            });
1474
1475                            ui.separator();
1476                            ui.label("Silence Notifications:");
1477                            if ui
1478                                .checkbox(
1479                                    &mut self.config.notification_silence_enabled,
1480                                    "Notify after prolonged silence",
1481                                )
1482                                .changed()
1483                            {
1484                                self.has_changes = true;
1485                                changes_this_frame = true;
1486                            }
1487
1488                            ui.horizontal(|ui| {
1489                                ui.label("Silence threshold (seconds):");
1490                                if ui
1491                                    .add(egui::Slider::new(
1492                                        &mut self.config.notification_silence_threshold,
1493                                        10..=600,
1494                                    ))
1495                                    .changed()
1496                                {
1497                                    self.has_changes = true;
1498                                    changes_this_frame = true;
1499                                }
1500                            });
1501
1502                            ui.separator();
1503                            ui.horizontal(|ui| {
1504                                ui.label("Max notification buffer:");
1505                                if ui
1506                                    .add(egui::Slider::new(
1507                                        &mut self.config.notification_max_buffer,
1508                                        10..=1000,
1509                                    ))
1510                                    .changed()
1511                                {
1512                                    self.has_changes = true;
1513                                    changes_this_frame = true;
1514                                }
1515                            });
1516                        });
1517                    }
1518
1519                    // Shell Configuration
1520                    if section_matches(
1521                        "Shell Configuration",
1522                        &["Custom shell", "Shell args", "Working directory"],
1523                    ) {
1524                        insert_section_separator(ui, &mut section_shown);
1525                        matches_found = true;
1526
1527                        ui.collapsing("Shell Configuration", |ui| {
1528                            ui.horizontal(|ui| {
1529                                ui.label("Custom shell (optional):");
1530                                if ui
1531                                    .text_edit_singleline(&mut self.temp_custom_shell)
1532                                    .changed()
1533                                {
1534                                    self.config.custom_shell = if self.temp_custom_shell.is_empty()
1535                                    {
1536                                        None
1537                                    } else {
1538                                        Some(self.temp_custom_shell.clone())
1539                                    };
1540                                    self.has_changes = true;
1541                                }
1542
1543                                if ui.button("Browse…").clicked()
1544                                    && let Some(path) = self.pick_file_path("Select shell binary")
1545                                {
1546                                    self.temp_custom_shell = path.clone();
1547                                    self.config.custom_shell = Some(path);
1548                                    self.has_changes = true;
1549                                }
1550                            });
1551
1552                            ui.horizontal(|ui| {
1553                                ui.label("Shell args (space-separated):");
1554                                if ui.text_edit_singleline(&mut self.temp_shell_args).changed() {
1555                                    self.config.shell_args = if self.temp_shell_args.is_empty() {
1556                                        None
1557                                    } else {
1558                                        Some(
1559                                            self.temp_shell_args
1560                                                .split_whitespace()
1561                                                .map(String::from)
1562                                                .collect(),
1563                                        )
1564                                    };
1565                                    self.has_changes = true;
1566                                }
1567                            });
1568
1569                            ui.horizontal(|ui| {
1570                                ui.label("Working directory (optional):");
1571                                if ui
1572                                    .text_edit_singleline(&mut self.temp_working_directory)
1573                                    .changed()
1574                                {
1575                                    self.config.working_directory =
1576                                        if self.temp_working_directory.is_empty() {
1577                                            None
1578                                        } else {
1579                                            Some(self.temp_working_directory.clone())
1580                                        };
1581                                    self.has_changes = true;
1582                                }
1583
1584                                if ui.button("Browse…").clicked()
1585                                    && let Some(path) =
1586                                        self.pick_folder_path("Select working directory")
1587                                {
1588                                    self.temp_working_directory = path.clone();
1589                                    self.config.working_directory = Some(path);
1590                                    self.has_changes = true;
1591                                }
1592                            });
1593                        });
1594                    }
1595
1596                    // Screenshot
1597                    if section_matches("Screenshot", &["Format", "png", "jpeg", "svg", "html"]) {
1598                        insert_section_separator(ui, &mut section_shown);
1599                        matches_found = true;
1600
1601                        ui.collapsing("Screenshot", |ui| {
1602                            ui.horizontal(|ui| {
1603                                ui.label("Format:");
1604
1605                                let options = ["png", "jpeg", "svg", "html"];
1606                                let mut selected = self.config.screenshot_format.clone();
1607
1608                                egui::ComboBox::from_id_salt("screenshot_format")
1609                                    .width(140.0)
1610                                    .selected_text(selected.as_str())
1611                                    .show_ui(ui, |ui| {
1612                                        for opt in options {
1613                                            ui.selectable_value(
1614                                                &mut selected,
1615                                                opt.to_string(),
1616                                                opt,
1617                                            );
1618                                        }
1619                                    });
1620
1621                                if selected != self.config.screenshot_format {
1622                                    self.config.screenshot_format = selected;
1623                                    self.has_changes = true;
1624                                    changes_this_frame = true;
1625                                }
1626                            });
1627                            ui.label("Supported: png, jpeg, svg, html");
1628                        });
1629                    }
1630
1631                    if !matches_found && !query.is_empty() {
1632                        ui.label(format!("No settings match \"{}\"", self.search_query));
1633                    }
1634                });
1635
1636                // Fixed footer with action buttons (outside ScrollArea)
1637                ui.separator();
1638                ui.horizontal(|ui| {
1639                    if ui.button("Save").clicked() {
1640                        save_requested = true;
1641                    }
1642
1643                    if ui.button("Discard").clicked() {
1644                        discard_requested = true;
1645                    }
1646
1647                    if ui.button("Close").clicked() {
1648                        close_requested = true;
1649                    }
1650
1651                    if self.has_changes {
1652                        ui.colored_label(egui::Color32::YELLOW, "* Unsaved changes");
1653                    }
1654                });
1655            });
1656        }
1657
1658        // Show shader editor window if visible
1659        if self.shader_editor_visible {
1660            let mut shader_editor_open = true;
1661            let mut apply_clicked = false;
1662            let mut cancel_clicked = false;
1663            let mut save_to_file_clicked = false;
1664
1665            // Calculate 90% of viewport height
1666            let viewport = ctx.input(|i| i.viewport_rect());
1667            let window_height = viewport.height() * 0.9;
1668
1669            Window::new("Shader Editor")
1670                .resizable(true)
1671                .default_width(900.0)
1672                .default_height(window_height)
1673                .default_pos(viewport.center())
1674                .pivot(egui::Align2::CENTER_CENTER)
1675                .open(&mut shader_editor_open)
1676                .frame(
1677                    Frame::window(&ctx.style())
1678                        .fill(Color32::from_rgba_unmultiplied(20, 20, 20, 255))
1679                        .stroke(egui::Stroke::new(1.0, Color32::from_rgb(60, 60, 60)))
1680                        .shadow(Shadow {
1681                            offset: [2, 2],
1682                            blur: 8,
1683                            spread: 0,
1684                            color: Color32::from_black_alpha(128),
1685                        }),
1686                )
1687                .show(ctx, |ui| {
1688                    ui.heading("GLSL Shader Editor (F11 to toggle)");
1689                    ui.horizontal(|ui| {
1690                        ui.label("Edit your custom shader below. Click Apply to test changes.");
1691                        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
1692                            ui.small("Ctrl+F to search");
1693                        });
1694                    });
1695                    ui.separator();
1696
1697                    // Search bar (Ctrl+F to toggle)
1698                    let ctrl_f = ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::F));
1699                    let escape = ui.input(|i| i.key_pressed(egui::Key::Escape));
1700
1701                    if ctrl_f {
1702                        self.shader_search_visible = !self.shader_search_visible;
1703                        if self.shader_search_visible {
1704                            // Focus will be requested below
1705                        }
1706                    }
1707                    if escape && self.shader_search_visible {
1708                        self.shader_search_visible = false;
1709                    }
1710
1711                    if self.shader_search_visible {
1712                        ui.horizontal(|ui| {
1713                            ui.label("Find:");
1714                            let search_field = ui.add(
1715                                egui::TextEdit::singleline(&mut self.shader_search_query)
1716                                    .desired_width(200.0)
1717                                    .hint_text("Search..."),
1718                            );
1719
1720                            // Focus search field when first shown
1721                            if ctrl_f {
1722                                search_field.request_focus();
1723                            }
1724
1725                            // Update matches when query changes
1726                            if search_field.changed() {
1727                                self.update_shader_search_matches();
1728                            }
1729
1730                            // Handle Enter for next match, Shift+Enter for previous
1731                            let enter_pressed = ui.input(|i| i.key_pressed(egui::Key::Enter));
1732                            let shift_held = ui.input(|i| i.modifiers.shift);
1733
1734                            // Previous/Next buttons
1735                            let has_matches = !self.shader_search_matches.is_empty();
1736                            if ui
1737                                .add_enabled(has_matches, egui::Button::new("◀"))
1738                                .on_hover_text("Previous (Shift+Enter)")
1739                                .clicked()
1740                                || (enter_pressed && shift_held && has_matches)
1741                            {
1742                                self.shader_search_previous();
1743                            }
1744                            if ui
1745                                .add_enabled(has_matches, egui::Button::new("▶"))
1746                                .on_hover_text("Next (Enter)")
1747                                .clicked()
1748                                || (enter_pressed && !shift_held && has_matches)
1749                            {
1750                                self.shader_search_next();
1751                            }
1752
1753                            // Match count
1754                            if self.shader_search_query.is_empty() {
1755                                ui.label("");
1756                            } else if self.shader_search_matches.is_empty() {
1757                                ui.colored_label(Color32::from_rgb(255, 100, 100), "No matches");
1758                            } else {
1759                                ui.label(format!(
1760                                    "{} / {}",
1761                                    self.shader_search_current + 1,
1762                                    self.shader_search_matches.len()
1763                                ));
1764                            }
1765
1766                            // Close button
1767                            if ui.button("✕").on_hover_text("Close (Esc)").clicked() {
1768                                self.shader_search_visible = false;
1769                            }
1770                        });
1771                        ui.separator();
1772                    }
1773
1774                    // Show error dialog if there's an error
1775                    let mut dismiss_error = false;
1776                    if let Some(error) = &self.shader_editor_error {
1777                        let error_text = error.clone();
1778                        let shader_path =
1779                            crate::config::Config::shader_path(&self.temp_custom_shader);
1780                        let full_error =
1781                            format!("File: {}\n\n{}", shader_path.display(), error_text);
1782
1783                        ui.group(|ui| {
1784                            ui.horizontal(|ui| {
1785                                ui.colored_label(
1786                                    Color32::from_rgb(255, 100, 100),
1787                                    "⚠ Shader Compilation Error",
1788                                );
1789                                ui.with_layout(
1790                                    egui::Layout::right_to_left(egui::Align::Center),
1791                                    |ui| {
1792                                        if ui.button("Dismiss").clicked() {
1793                                            dismiss_error = true;
1794                                        }
1795                                        if ui.button("Copy").clicked()
1796                                            && let Ok(mut clipboard) = Clipboard::new()
1797                                        {
1798                                            let _ = clipboard.set_text(full_error.clone());
1799                                        }
1800                                    },
1801                                );
1802                            });
1803                            ui.label(format!("File: {}", shader_path.display()));
1804                            ui.separator();
1805                            // Multiline selectable text for copying
1806                            egui::ScrollArea::vertical()
1807                                .max_height(120.0)
1808                                .show(ui, |ui| {
1809                                    ui.add(
1810                                        egui::TextEdit::multiline(&mut error_text.as_str())
1811                                            .font(egui::TextStyle::Monospace)
1812                                            .desired_width(f32::INFINITY)
1813                                            .interactive(true),
1814                                    );
1815                                });
1816                        });
1817                        ui.separator();
1818                    }
1819                    if dismiss_error {
1820                        self.shader_editor_error = None;
1821                    }
1822
1823                    // Shader source editor
1824                    // Note: code_editor() provides a dark theme optimized for code
1825                    let available_height = ui.available_height() - 60.0; // Reserve space for buttons
1826
1827                    // Get current search match position before rendering
1828                    let search_selection = self.shader_search_current_pos().map(|pos| {
1829                        let end = pos + self.shader_search_query.len();
1830                        (pos, end)
1831                    });
1832
1833                    let editor_id = egui::Id::new("shader_editor_textedit");
1834
1835                    egui::ScrollArea::both()
1836                        .auto_shrink([false, false])
1837                        .max_height(available_height)
1838                        .show(ui, |ui| {
1839                            let response = ui.add(
1840                                egui::TextEdit::multiline(&mut self.shader_editor_source)
1841                                    .id(editor_id)
1842                                    .font(egui::TextStyle::Monospace)
1843                                    .code_editor()
1844                                    .desired_width(f32::INFINITY)
1845                                    .min_size(egui::vec2(
1846                                        ui.available_width(),
1847                                        available_height - 20.0,
1848                                    )),
1849                            );
1850
1851                            // If we have a search match, select it and scroll to it
1852                            if let Some((start, end)) = search_selection
1853                                && let Some(mut state) =
1854                                    egui::TextEdit::load_state(ui.ctx(), editor_id)
1855                            {
1856                                // Create a cursor range that selects the match
1857                                let ccursor_range = egui::text::CCursorRange::two(
1858                                    egui::text::CCursor::new(start),
1859                                    egui::text::CCursor::new(end),
1860                                );
1861                                state.cursor.set_char_range(Some(ccursor_range));
1862                                state.store(ui.ctx(), editor_id);
1863
1864                                // Request scroll to cursor
1865                                ui.scroll_to_rect(response.rect, Some(egui::Align::Center));
1866                            }
1867                        });
1868
1869                    ui.separator();
1870
1871                    // Action buttons
1872                    ui.horizontal(|ui| {
1873                        if ui.button("Apply").clicked() {
1874                            apply_clicked = true;
1875                        }
1876                        ui.label("|");
1877                        if ui.button("Save to File").clicked() {
1878                            save_to_file_clicked = true;
1879                        }
1880                        ui.label("|");
1881                        if ui.button("Find").on_hover_text("Ctrl+F").clicked() {
1882                            self.shader_search_visible = !self.shader_search_visible;
1883                        }
1884                        ui.label("|");
1885                        if ui.button("Revert").clicked() {
1886                            self.shader_editor_source = self.shader_editor_original.clone();
1887                            self.shader_editor_error = None;
1888                        }
1889                        ui.label("|");
1890                        if ui.button("Close").clicked() {
1891                            cancel_clicked = true;
1892                        }
1893                    });
1894                });
1895
1896            // Handle shader editor actions
1897            if apply_clicked {
1898                shader_apply_result = Some(ShaderEditorResult {
1899                    source: self.shader_editor_source.clone(),
1900                });
1901                // Don't close editor - let user see if it worked or get error
1902            }
1903
1904            if save_to_file_clicked {
1905                // Save current source to the shader file
1906                let shader_path = crate::config::Config::shader_path(&self.temp_custom_shader);
1907                match std::fs::write(&shader_path, &self.shader_editor_source) {
1908                    Ok(()) => {
1909                        self.shader_editor_original = self.shader_editor_source.clone();
1910                        log::info!("Shader saved to {}", shader_path.display());
1911                    }
1912                    Err(e) => {
1913                        self.shader_editor_error = Some(format!(
1914                            "Failed to save shader file '{}': {}",
1915                            shader_path.display(),
1916                            e
1917                        ));
1918                    }
1919                }
1920            }
1921
1922            if cancel_clicked || !shader_editor_open {
1923                self.shader_editor_visible = false;
1924                self.shader_editor_source.clear();
1925                self.shader_editor_original.clear();
1926                self.shader_editor_error = None;
1927                // Clear search state
1928                self.shader_search_query.clear();
1929                self.shader_search_matches.clear();
1930                self.shader_search_current = 0;
1931                self.shader_search_visible = false;
1932            }
1933        }
1934
1935        // Create Shader Dialog
1936        if self.show_create_shader_dialog {
1937            let mut close_dialog = false;
1938            let mut create_shader = false;
1939
1940            Window::new("Create New Shader")
1941                .collapsible(false)
1942                .resizable(false)
1943                .default_width(400.0)
1944                .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
1945                .show(ctx, |ui| {
1946                    ui.label("Enter a name for the new shader file:");
1947                    ui.label("(will be saved as .glsl in the shaders folder)");
1948                    ui.add_space(8.0);
1949
1950                    ui.horizontal(|ui| {
1951                        ui.label("Name:");
1952                        let response = ui.text_edit_singleline(&mut self.new_shader_name);
1953                        if response.lost_focus()
1954                            && ui.input(|i| i.key_pressed(egui::Key::Enter))
1955                            && !self.new_shader_name.is_empty()
1956                        {
1957                            create_shader = true;
1958                        }
1959                    });
1960
1961                    ui.add_space(8.0);
1962                    ui.horizontal(|ui| {
1963                        if ui.button("Create").clicked() && !self.new_shader_name.is_empty() {
1964                            create_shader = true;
1965                        }
1966                        if ui.button("Cancel").clicked() {
1967                            close_dialog = true;
1968                        }
1969                    });
1970                });
1971
1972            if create_shader {
1973                // Ensure filename ends with .glsl
1974                let mut filename = self.new_shader_name.clone();
1975                if !filename.ends_with(".glsl")
1976                    && !filename.ends_with(".frag")
1977                    && !filename.ends_with(".shader")
1978                {
1979                    filename.push_str(".glsl");
1980                }
1981
1982                let shader_path = crate::config::Config::shaders_dir().join(&filename);
1983
1984                // Check if file already exists
1985                if shader_path.exists() {
1986                    self.shader_editor_error =
1987                        Some(format!("Shader '{}' already exists!", filename));
1988                } else {
1989                    // Create the shader with a basic template
1990                    let template = r#"// Custom shader for par-term
1991// Available uniforms:
1992//   iTime       - Time in seconds (when animation enabled)
1993//   iResolution - Viewport resolution (vec2)
1994//   iChannel0   - Terminal content texture (sampler2D)
1995//   iOpacity    - Window opacity (float)
1996//   iTextOpacity - Text opacity (float)
1997
1998void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1999    vec2 uv = fragCoord / iResolution.xy;
2000
2001    // Sample terminal content
2002    vec4 terminal = texture(iChannel0, uv);
2003
2004    // Example: simple color tint based on position
2005    vec3 tint = vec3(0.8, 0.9, 1.0);
2006
2007    // Mix terminal content with effect
2008    vec3 color = terminal.rgb * tint;
2009
2010    fragColor = vec4(color, terminal.a);
2011}
2012"#;
2013
2014                    match std::fs::write(&shader_path, template) {
2015                        Ok(()) => {
2016                            log::info!("Created new shader: {}", shader_path.display());
2017                            // Update the shader list
2018                            self.refresh_shaders();
2019                            // Select the new shader
2020                            self.temp_custom_shader = filename.clone();
2021                            self.config.custom_shader = Some(filename);
2022                            self.has_changes = true;
2023                            // Open the shader editor with the new shader
2024                            self.shader_editor_source = template.to_string();
2025                            self.shader_editor_original = template.to_string();
2026                            self.shader_editor_error = None;
2027                            self.shader_editor_visible = true;
2028                            close_dialog = true;
2029                        }
2030                        Err(e) => {
2031                            self.shader_editor_error =
2032                                Some(format!("Failed to create shader: {}", e));
2033                        }
2034                    }
2035                }
2036            }
2037
2038            if close_dialog {
2039                self.show_create_shader_dialog = false;
2040                self.new_shader_name.clear();
2041            }
2042        }
2043
2044        // Delete Shader Confirmation Dialog
2045        if self.show_delete_shader_dialog {
2046            let mut close_dialog = false;
2047            let mut delete_shader = false;
2048            let shader_name = self.temp_custom_shader.clone();
2049
2050            Window::new("Delete Shader")
2051                .collapsible(false)
2052                .resizable(false)
2053                .default_width(350.0)
2054                .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
2055                .show(ctx, |ui| {
2056                    ui.label(format!(
2057                        "Are you sure you want to delete '{}'?",
2058                        shader_name
2059                    ));
2060                    ui.label("This action cannot be undone.");
2061                    ui.add_space(12.0);
2062                    ui.horizontal(|ui| {
2063                        if ui.button("Delete").clicked() {
2064                            delete_shader = true;
2065                        }
2066                        if ui.button("Cancel").clicked() {
2067                            close_dialog = true;
2068                        }
2069                    });
2070                });
2071
2072            if delete_shader {
2073                let shader_path = crate::config::Config::shader_path(&shader_name);
2074                match std::fs::remove_file(&shader_path) {
2075                    Ok(()) => {
2076                        log::info!("Deleted shader: {}", shader_path.display());
2077                        // Clear the selection
2078                        self.temp_custom_shader.clear();
2079                        self.config.custom_shader = None;
2080                        self.has_changes = true;
2081                        // Refresh the shader list
2082                        self.refresh_shaders();
2083                        close_dialog = true;
2084                    }
2085                    Err(e) => {
2086                        self.shader_editor_error = Some(format!("Failed to delete shader: {}", e));
2087                        close_dialog = true;
2088                    }
2089                }
2090            }
2091
2092            if close_dialog {
2093                self.show_delete_shader_dialog = false;
2094            }
2095        }
2096
2097        // Update visibility based on window state (only if settings window is being shown)
2098        if self.visible && (!open || close_requested) {
2099            self.visible = false;
2100        }
2101
2102        // Handle save request
2103        let config_to_save = if save_requested {
2104            if self.font_pending_changes {
2105                self.apply_font_changes();
2106            }
2107            self.has_changes = false;
2108            Some(self.config.clone())
2109        } else {
2110            None
2111        };
2112
2113        // Handle discard request
2114        if discard_requested {
2115            self.has_changes = false;
2116            self.sync_font_temps_from_config();
2117            // No-op for live updates when discarded
2118        }
2119
2120        // Push live config while the settings window is open to guarantee real-time updates.
2121        let config_for_live_update = if self.visible {
2122            // Only log when the value actually changes to avoid spam
2123            if (self.config.window_opacity - self.last_live_opacity).abs() > f32::EPSILON {
2124                log::info!(
2125                    "SettingsUI: live opacity {:.3} (last {:.3})",
2126                    self.config.window_opacity,
2127                    self.last_live_opacity
2128                );
2129                self.last_live_opacity = self.config.window_opacity;
2130            }
2131            Some(self.config.clone())
2132        } else {
2133            None
2134        };
2135
2136        (config_to_save, config_for_live_update, shader_apply_result)
2137    }
2138}
2139
2140// Note: Syntax highlighting for shader editor could be added via egui_extras::syntax_highlighting
2141// when the API stabilizes. The code_editor() mode provides a dark theme suitable for code editing.