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