Skip to main content

par_term_settings_ui/
lib.rs

1//! Settings UI for par-term terminal emulator.
2//!
3//! This crate provides an egui-based settings interface for configuring
4//! terminal options at runtime. It is designed to be decoupled from the
5//! main terminal implementation through trait interfaces.
6
7use egui::{Color32, Context, Frame, Window, epaint::Shadow};
8use par_term_config::{
9    BackgroundImageMode, Config, CursorShaderMetadataCache, ShaderMetadataCache,
10};
11use par_term_config::{Profile, ProfileId};
12use rfd::FileDialog;
13use std::collections::HashSet;
14
15/// Callback type for detecting modified bundled shaders.
16pub type ShaderDetectModifiedFn = fn() -> Result<Vec<String>, String>;
17
18// Trait interfaces for decoupling from main crate
19mod traits;
20pub use traits::*;
21
22// Window arrangements (pure data types)
23pub mod arrangements;
24pub use arrangements::{
25    ArrangementId, ArrangementManager, MonitorInfo, TabSnapshot, WindowArrangement, WindowSnapshot,
26};
27
28// Shell detection utility
29pub mod shell_detection;
30
31// Profile management modal UI
32pub mod profile_modal_ui;
33pub use profile_modal_ui::{ProfileModalAction, ProfileModalUI};
34
35// Reorganized settings tabs
36pub mod actions_tab;
37pub mod advanced_tab;
38pub mod ai_inspector_tab;
39pub mod appearance_tab;
40pub mod arrangements_tab;
41pub mod automation_tab;
42pub mod badge_tab;
43pub mod effects_tab;
44pub mod input_tab;
45pub mod integrations_tab;
46pub mod notifications_tab;
47pub mod profiles_tab;
48pub mod progress_bar_tab;
49pub mod quick_settings;
50pub mod scripts_tab;
51pub mod section;
52pub mod sidebar;
53pub mod snippets_tab;
54pub mod ssh_tab;
55pub mod status_bar_tab;
56pub mod terminal_tab;
57pub mod window_tab;
58
59// Background tab is still needed by effects_tab for delegation
60pub mod background_tab;
61
62// Shader editor components (used by background_tab)
63mod cursor_shader_editor;
64mod shader_dialogs;
65mod shader_editor;
66mod shader_utils;
67
68pub use sidebar::SettingsTab;
69
70// Re-export types that settings consumers need
71pub use par_term_config::{
72    self as config, BackgroundImageMode as BgMode, CursorShaderMetadataCache as CursorShaderCache,
73    ProfileManager, ProfileSource, ShaderMetadataCache as ShaderCache, Theme, VsyncMode,
74};
75
76/// Result of shader editor actions
77#[derive(Debug, Clone)]
78pub struct ShaderEditorResult {
79    /// New shader source code to compile and apply
80    pub source: String,
81}
82
83/// Result of cursor shader editor actions
84#[derive(Debug, Clone)]
85pub struct CursorShaderEditorResult {
86    /// New cursor shader source code to compile and apply
87    pub source: String,
88}
89
90/// Result of processing a settings window event.
91///
92/// This enum bridges the settings UI crate with the main application.
93/// The main application processes these actions after events are handled
94/// by the settings window.
95#[derive(Debug, Clone)]
96pub enum SettingsWindowAction {
97    /// No action needed
98    None,
99    /// Close the settings window
100    Close,
101    /// Apply config changes to terminal windows (live update)
102    ApplyConfig(Config),
103    /// Save config to disk
104    SaveConfig(Config),
105    /// Apply background shader from editor
106    ApplyShader(ShaderEditorResult),
107    /// Apply cursor shader from editor
108    ApplyCursorShader(CursorShaderEditorResult),
109    /// Send a test notification to verify permissions
110    TestNotification,
111    /// Save profiles from inline editor to all windows
112    SaveProfiles(Vec<Profile>),
113    /// Open a profile in the focused terminal window
114    OpenProfile(ProfileId),
115    /// Start a coprocess by config index on the active tab
116    StartCoprocess(usize),
117    /// Stop a coprocess by config index on the active tab
118    StopCoprocess(usize),
119    /// Start a script by config index on the active tab
120    StartScript(usize),
121    /// Stop a script by config index on the active tab
122    StopScript(usize),
123    /// Open the debug log file in the system's default editor/viewer
124    OpenLogFile,
125    /// Save the current window layout as an arrangement
126    SaveArrangement(String),
127    /// Restore a saved window arrangement
128    RestoreArrangement(ArrangementId),
129    /// Delete a saved window arrangement
130    DeleteArrangement(ArrangementId),
131    /// Rename a saved window arrangement
132    RenameArrangement(ArrangementId, String),
133    /// User requested an immediate update check
134    ForceUpdateCheck,
135    /// User requested to install the available update
136    InstallUpdate(String),
137    /// Flash pane indices on the terminal window
138    IdentifyPanes,
139    /// Install shell integration for the detected shell
140    InstallShellIntegration,
141    /// Uninstall shell integration from all shells
142    UninstallShellIntegration,
143}
144
145/// Lightweight information about a saved arrangement (used by trait interface).
146#[derive(Debug, Clone)]
147pub struct ArrangementInfo {
148    /// Unique identifier
149    pub id: ArrangementId,
150    /// Display name
151    pub name: String,
152    /// Number of windows in the arrangement
153    pub window_count: usize,
154}
155
156/// Result of a shader installation operation.
157#[derive(Debug, Clone)]
158pub struct ShaderInstallResult {
159    /// Number of shaders installed
160    pub installed: usize,
161    /// Number of shaders skipped (unchanged)
162    pub skipped: usize,
163    /// Number of obsolete shaders removed
164    pub removed: usize,
165}
166
167/// Result of a shader uninstallation operation.
168#[derive(Debug, Clone)]
169pub struct ShaderUninstallResult {
170    /// Number of shaders removed
171    pub removed: usize,
172    /// Number of modified shaders kept
173    pub kept: usize,
174    /// Whether confirmation is needed for modified files
175    pub needs_confirmation: bool,
176}
177
178/// Result of shell integration installation.
179#[derive(Debug, Clone)]
180pub struct ShellIntegrationInstallResult {
181    /// Shell type that was configured
182    pub shell: String,
183    /// Path to the integration script
184    pub script_path: String,
185    /// RC file that was modified
186    pub rc_file: String,
187    /// Whether a shell restart is needed
188    pub needs_restart: bool,
189}
190
191/// Result of shell integration uninstallation.
192#[derive(Debug, Clone)]
193pub struct ShellIntegrationUninstallResult {
194    /// Whether rc files were cleaned
195    pub cleaned: bool,
196    /// Whether manual intervention is needed
197    pub needs_manual: bool,
198    /// Number of integration scripts removed
199    pub scripts_removed: usize,
200}
201
202/// Result of a self-update operation.
203#[derive(Debug, Clone)]
204pub struct UpdateResult {
205    /// Old version before the update
206    pub old_version: String,
207    /// New version after the update
208    pub new_version: String,
209    /// Path where the binary was installed
210    pub install_path: String,
211    /// Whether a restart is needed
212    pub needs_restart: bool,
213}
214
215/// Installation type detected for the running binary.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum InstallationType {
218    /// Installed via Homebrew
219    Homebrew,
220    /// Installed via `cargo install`
221    CargoInstall,
222    /// Running from a macOS .app bundle
223    MacOSBundle,
224    /// Standalone binary
225    StandaloneBinary,
226}
227
228/// Result of an update check.
229#[derive(Debug, Clone)]
230pub enum UpdateCheckResult {
231    /// No update available, up to date
232    UpToDate,
233    /// An update is available
234    UpdateAvailable(UpdateCheckInfo),
235    /// Update checking is disabled
236    Disabled,
237    /// Check was skipped (cooldown not elapsed)
238    Skipped,
239    /// Error occurred during check
240    Error(String),
241}
242
243/// Information about an available update.
244#[derive(Debug, Clone)]
245pub struct UpdateCheckInfo {
246    /// Version string (e.g., "0.16.0")
247    pub version: String,
248    /// Release notes/changelog
249    pub release_notes: Option<String>,
250    /// URL to release page
251    pub release_url: String,
252    /// When the release was published
253    pub published_at: Option<String>,
254}
255
256/// Format a timestamp string for display in the UI.
257pub fn format_timestamp(timestamp: &str) -> String {
258    match chrono::DateTime::parse_from_rfc3339(timestamp) {
259        Ok(dt) => dt.format("%Y-%m-%d %H:%M").to_string(),
260        Err(_) => timestamp.to_string(),
261    }
262}
263
264/// Get the path to the debug log file.
265pub fn log_path() -> std::path::PathBuf {
266    #[cfg(unix)]
267    {
268        std::path::PathBuf::from("/tmp/par_term_debug.log")
269    }
270    #[cfg(windows)]
271    {
272        std::env::temp_dir().join("par_term_debug.log")
273    }
274}
275
276/// Create a configured HTTP agent for URL fetching.
277pub fn http_agent() -> ureq::Agent {
278    ureq::Agent::new_with_defaults()
279}
280
281/// Settings UI manager using egui
282pub struct SettingsUI {
283    /// Whether the settings window is currently visible
284    pub visible: bool,
285
286    /// Working copy of config being edited
287    pub config: Config,
288
289    /// Last opacity value that was forwarded for live updates
290    pub last_live_opacity: f32,
291
292    /// Whether config has unsaved changes
293    pub has_changes: bool,
294
295    /// Temp strings for optional fields (for UI editing)
296    pub temp_font_bold: String,
297    pub temp_font_italic: String,
298    pub temp_font_bold_italic: String,
299    pub temp_font_family: String,
300    pub temp_font_size: f32,
301    pub temp_line_spacing: f32,
302    pub temp_char_spacing: f32,
303    pub temp_enable_text_shaping: bool,
304    pub temp_enable_ligatures: bool,
305    pub temp_enable_kerning: bool,
306    pub font_pending_changes: bool,
307    pub temp_custom_shell: String,
308    pub temp_shell_args: String,
309    pub temp_working_directory: String,
310    pub temp_startup_directory: String,
311    pub temp_initial_text: String,
312    pub temp_background_image: String,
313    pub temp_custom_shader: String,
314    pub temp_cursor_shader: String,
315
316    /// Temporary strings for shader channel texture paths (iChannel0-3)
317    pub temp_shader_channel0: String,
318    pub temp_shader_channel1: String,
319    pub temp_shader_channel2: String,
320    pub temp_shader_channel3: String,
321
322    /// Temporary string for cubemap path prefix (iCubemap)
323    pub temp_cubemap_path: String,
324
325    /// Temporary color for solid background color editing
326    pub temp_background_color: [u8; 3],
327
328    /// Temporary per-pane background image path for editing
329    pub temp_pane_bg_path: String,
330    /// Temporary per-pane background mode
331    pub temp_pane_bg_mode: BackgroundImageMode,
332    /// Temporary per-pane background opacity
333    pub temp_pane_bg_opacity: f32,
334    /// Temporary per-pane background darken amount
335    pub temp_pane_bg_darken: f32,
336    /// Index of the pane currently being configured (None = no pane selected)
337    pub temp_pane_bg_index: Option<usize>,
338
339    /// Search query used to filter settings sections
340    pub search_query: String,
341    /// Whether to focus the search input on next frame
342    pub focus_search: bool,
343
344    // Background shader editor state
345    /// Whether the shader editor window is visible
346    pub shader_editor_visible: bool,
347    /// The shader source code being edited
348    pub shader_editor_source: String,
349    /// Shader compilation error message (if any)
350    pub shader_editor_error: Option<String>,
351    /// Original source when editor was opened (for cancel)
352    pub shader_editor_original: String,
353
354    // Cursor shader editor state
355    /// Whether the cursor shader editor window is visible
356    pub cursor_shader_editor_visible: bool,
357    /// The cursor shader source code being edited
358    pub cursor_shader_editor_source: String,
359    /// Cursor shader compilation error message (if any)
360    pub cursor_shader_editor_error: Option<String>,
361    /// Original cursor shader source when editor was opened (for cancel)
362    pub cursor_shader_editor_original: String,
363
364    // Agent state
365    /// Available agent identities for the AI Inspector dropdown (identity, name)
366    pub available_agent_ids: Vec<(String, String)>,
367
368    // Shader management state
369    /// List of available shader files in the shaders folder
370    pub available_shaders: Vec<String>,
371    /// List of available cubemap prefixes (e.g., "textures/cubemaps/env-outside")
372    pub available_cubemaps: Vec<String>,
373    /// Name for new shader (in create dialog)
374    pub new_shader_name: String,
375    /// Whether to show the create shader dialog
376    pub show_create_shader_dialog: bool,
377    /// Whether to show the delete confirmation dialog
378    pub show_delete_shader_dialog: bool,
379
380    // Shader editor search state
381    /// Search query for shader editor
382    pub shader_search_query: String,
383    /// Byte positions of search matches
384    pub shader_search_matches: Vec<usize>,
385    /// Current match index (0-based)
386    pub shader_search_current: usize,
387    /// Whether search bar is visible
388    pub shader_search_visible: bool,
389
390    // Per-shader configuration state
391    /// Cache for parsed shader metadata
392    pub shader_metadata_cache: ShaderMetadataCache,
393    /// Cache for parsed cursor shader metadata
394    pub cursor_shader_metadata_cache: CursorShaderMetadataCache,
395    /// Whether the per-shader settings section is expanded
396    pub shader_settings_expanded: bool,
397    /// Whether the per-cursor-shader settings section is expanded
398    pub cursor_shader_settings_expanded: bool,
399
400    // Current window state (for "Use Current Size" button)
401    /// Current terminal columns (actual rendered size, may differ from config)
402    pub current_cols: usize,
403    /// Current terminal rows (actual rendered size, may differ from config)
404    pub current_rows: usize,
405
406    // VSync mode support (for runtime validation)
407    /// Supported vsync modes for the current display
408    pub supported_vsync_modes: Vec<par_term_config::VsyncMode>,
409    /// Warning message when an unsupported vsync mode is selected
410    pub vsync_warning: Option<String>,
411
412    // Keybinding recording state
413    /// Index of the keybinding currently being recorded (None = not recording)
414    pub keybinding_recording_index: Option<usize>,
415    /// The recorded key combination string (displayed during recording)
416    pub keybinding_recorded_combo: Option<String>,
417
418    // Notification test state
419    /// Flag to request sending a test notification
420    pub test_notification_requested: bool,
421
422    // New UI state for reorganized settings
423    /// Currently selected settings tab (new sidebar navigation)
424    pub selected_tab: SettingsTab,
425    /// Set of collapsed section IDs (sections start open by default, collapsed when user collapses them)
426    pub collapsed_sections: HashSet<String>,
427
428    // Integrations tab action state
429    /// Pending shell integration action (install/uninstall)
430    pub shell_integration_action: Option<integrations_tab::ShellIntegrationAction>,
431
432    // Profiles tab inline management state
433    /// Inline profile management UI (embedded in Profiles tab)
434    pub profile_modal_ui: ProfileModalUI,
435    /// Flag: profile save was requested from inline UI
436    pub profile_save_requested: bool,
437    /// Flag: open a profile was requested from inline UI
438    pub profile_open_requested: Option<ProfileId>,
439    // Shader install workflow state
440    /// Whether a shader install/uninstall operation is running
441    shader_installing: bool,
442    /// Status message for shader installs
443    shader_status: Option<String>,
444    /// Error message for shader installs
445    shader_error: Option<String>,
446    /// Whether to show overwrite prompt for modified bundled shaders
447    shader_overwrite_prompt_visible: bool,
448    /// List of modified bundled shader files
449    shader_conflicts: Vec<String>,
450    /// Channel receiver for async shader installs
451    shader_install_receiver: Option<std::sync::mpsc::Receiver<Result<ShaderInstallResult, String>>>,
452
453    // Automation tab state
454    /// Index of trigger currently being edited (None = not editing)
455    pub editing_trigger_index: Option<usize>,
456    /// Temporary trigger name for edit form
457    pub temp_trigger_name: String,
458    /// Temporary trigger regex pattern for edit form
459    pub temp_trigger_pattern: String,
460    /// Temporary trigger actions for edit form
461    pub temp_trigger_actions: Vec<par_term_config::automation::TriggerActionConfig>,
462    /// Whether the add-new-trigger form is active
463    pub adding_new_trigger: bool,
464    /// Regex validation error for trigger pattern
465    pub trigger_pattern_error: Option<String>,
466    /// Index of coprocess currently being edited (None = not editing)
467    pub editing_coprocess_index: Option<usize>,
468    /// Temporary coprocess name for edit form
469    pub temp_coprocess_name: String,
470    /// Temporary coprocess command for edit form
471    pub temp_coprocess_command: String,
472    /// Temporary coprocess args for edit form
473    pub temp_coprocess_args: String,
474    /// Temporary coprocess auto_start for edit form
475    pub temp_coprocess_auto_start: bool,
476    /// Temporary coprocess copy_terminal_output for edit form
477    pub temp_coprocess_copy_output: bool,
478    /// Temporary coprocess restart policy for edit form
479    pub temp_coprocess_restart_policy: par_term_config::automation::RestartPolicy,
480    /// Temporary coprocess restart delay for edit form
481    pub temp_coprocess_restart_delay_ms: u64,
482    /// Whether the add-new-coprocess form is active
483    pub adding_new_coprocess: bool,
484    /// Flag to request trigger resync after save
485    pub trigger_resync_requested: bool,
486    /// Pending coprocess start/stop actions: (config_index, start=true/stop=false)
487    pub pending_coprocess_actions: Vec<(usize, bool)>,
488    /// Running state of coprocesses (indexed by config position, updated by main window)
489    pub coprocess_running: Vec<bool>,
490    /// Last error messages per coprocess (indexed by config position, updated by main window)
491    pub coprocess_errors: Vec<String>,
492    /// Buffered stdout output per coprocess (indexed by config position, drained from core)
493    pub coprocess_output: Vec<Vec<String>>,
494    /// Which coprocess output viewers are expanded (indexed by config position)
495    pub coprocess_output_expanded: Vec<bool>,
496    // === Script management state ===
497    /// Index of script currently being edited (None = not editing)
498    pub editing_script_index: Option<usize>,
499    /// Temporary script name for edit form
500    pub temp_script_name: String,
501    /// Temporary script path for edit form
502    pub temp_script_path: String,
503    /// Temporary script args for edit form
504    pub temp_script_args: String,
505    /// Temporary script auto_start for edit form
506    pub temp_script_auto_start: bool,
507    /// Temporary script enabled for edit form
508    pub temp_script_enabled: bool,
509    /// Temporary script restart policy for edit form
510    pub temp_script_restart_policy: par_term_config::automation::RestartPolicy,
511    /// Temporary script restart delay for edit form
512    pub temp_script_restart_delay_ms: u64,
513    /// Temporary script subscriptions for edit form (comma-separated)
514    pub temp_script_subscriptions: String,
515    /// Whether the add-new-script form is active
516    pub adding_new_script: bool,
517    /// Pending script start/stop actions: (config_index, start=true/stop=false)
518    pub pending_script_actions: Vec<(usize, bool)>,
519    /// Running state of scripts (indexed by config position, updated by main window)
520    pub script_running: Vec<bool>,
521    /// Last error messages per script (indexed by config position, updated by main window)
522    pub script_errors: Vec<String>,
523    /// Buffered output per script (indexed by config position, drained from script manager)
524    pub script_output: Vec<Vec<String>>,
525    /// Which script output viewers are expanded (indexed by config position)
526    pub script_output_expanded: Vec<bool>,
527    /// Panel state per script: (title, content) from SetPanel commands
528    pub script_panels: Vec<Option<(String, String)>>,
529
530    /// Flag to request opening the debug log file
531    pub open_log_requested: bool,
532
533    /// Flag to request identifying panes (flash indices on terminal window)
534    pub identify_panes_requested: bool,
535
536    // Self-update state
537    /// User requested to install the available update
538    pub update_install_requested: bool,
539    /// User requested an immediate update check
540    pub check_now_requested: bool,
541    /// Status text for update UI display
542    pub update_status: Option<String>,
543    /// Result of self-update operation
544    pub update_result: Option<Result<UpdateResult, String>>,
545    /// Last update check result (synced from WindowManager)
546    pub last_update_result: Option<UpdateCheckResult>,
547    /// Whether an update install is in progress
548    pub update_installing: bool,
549    /// Channel receiver for async update installs
550    update_install_receiver: Option<std::sync::mpsc::Receiver<Result<UpdateResult, String>>>,
551
552    // Snippets tab state
553    /// Index of snippet currently being edited (None = not editing)
554    pub editing_snippet_index: Option<usize>,
555    /// Temporary snippet ID for edit form
556    pub temp_snippet_id: String,
557    /// Temporary snippet title for edit form
558    pub temp_snippet_title: String,
559    /// Temporary snippet content for edit form
560    pub temp_snippet_content: String,
561    /// Temporary snippet keybinding for edit form
562    pub temp_snippet_keybinding: String,
563    /// Temporary snippet folder for edit form
564    pub temp_snippet_folder: String,
565    /// Temporary snippet description for edit form
566    pub temp_snippet_description: String,
567    /// Temporary snippet keybinding enabled for edit form
568    pub temp_snippet_keybinding_enabled: bool,
569    /// Temporary snippet auto_execute for edit form
570    pub temp_snippet_auto_execute: bool,
571    /// Temporary snippet custom variables for edit form (ordered pairs for stable UI)
572    pub temp_snippet_variables: Vec<(String, String)>,
573    /// Whether the add-new-snippet form is active
574    pub adding_new_snippet: bool,
575    /// Whether currently recording a keybinding for a snippet
576    pub recording_snippet_keybinding: bool,
577    /// Recorded keybinding combo for snippet (displayed during recording)
578    pub snippet_recorded_combo: Option<String>,
579
580    // Actions tab state
581    /// Index of action currently being edited (None = not editing)
582    pub editing_action_index: Option<usize>,
583    /// Temporary action type for edit form (0=ShellCommand, 1=InsertText, 2=KeySequence)
584    pub temp_action_type: usize,
585    /// Temporary action ID for edit form
586    pub temp_action_id: String,
587    /// Temporary action title for edit form
588    pub temp_action_title: String,
589    /// Temporary action command (for ShellCommand type)
590    pub temp_action_command: String,
591    /// Temporary action args (for ShellCommand type)
592    pub temp_action_args: String,
593    /// Temporary action text (for InsertText type)
594    pub temp_action_text: String,
595    /// Temporary action keys (for KeySequence type)
596    pub temp_action_keys: String,
597    /// Temporary action keybinding for edit form
598    pub temp_action_keybinding: String,
599    /// Whether the add-new-action form is active
600    pub adding_new_action: bool,
601    /// Whether currently recording a keybinding for an action
602    pub recording_action_keybinding: bool,
603    /// Recorded keybinding combo for action (displayed during recording)
604    pub action_recorded_combo: Option<String>,
605
606    // Dynamic profile sources editing state
607    /// Index of dynamic source currently being edited (None = not editing)
608    pub dynamic_source_editing: Option<usize>,
609    /// Temp copy of the source being edited
610    pub dynamic_source_edit_buffer: Option<par_term_config::DynamicProfileSource>,
611    /// Temp buffer for new header key being added
612    pub dynamic_source_new_header_key: String,
613    /// Temp buffer for new header value being added
614    pub dynamic_source_new_header_value: String,
615
616    // Import/export preferences state
617    /// Temporary URL for import-from-URL feature
618    pub temp_import_url: String,
619    /// Status message for import/export operations
620    pub import_export_status: Option<String>,
621    /// Whether the import/export status is an error (true) or success (false)
622    pub import_export_is_error: bool,
623
624    // Reset to defaults dialog state
625    /// Whether to show the reset to defaults confirmation dialog
626    pub show_reset_defaults_dialog: bool,
627
628    // Arrangements tab state
629    /// Name for saving a new arrangement
630    pub arrangement_save_name: String,
631    /// Arrangement ID pending restore confirmation
632    pub arrangement_confirm_restore: Option<ArrangementId>,
633    /// Arrangement ID pending delete confirmation
634    pub arrangement_confirm_delete: Option<ArrangementId>,
635    /// Name pending overwrite confirmation (when saving with duplicate name)
636    pub arrangement_confirm_overwrite: Option<String>,
637    /// Arrangement ID being renamed
638    pub arrangement_rename_id: Option<ArrangementId>,
639    /// Text buffer for rename operation
640    pub arrangement_rename_text: String,
641    /// Pending arrangement actions to send to the main window
642    pub pending_arrangement_actions: Vec<SettingsWindowAction>,
643    /// Cached arrangement manager data (synced from WindowManager)
644    pub arrangement_manager: ArrangementManager,
645
646    // Callbacks for main-crate operations
647    /// Application version string (set by main crate via env!("CARGO_PKG_VERSION"))
648    pub app_version: &'static str,
649
650    /// Detected installation type (set by main crate)
651    pub installation_type: InstallationType,
652
653    /// Callback: install shaders with manifest (set by main crate)
654    pub shader_install_fn: Option<fn(bool) -> Result<ShaderInstallResult, String>>,
655
656    /// Callback: detect modified bundled shaders
657    pub shader_detect_modified_fn: Option<ShaderDetectModifiedFn>,
658
659    /// Callback: uninstall shaders
660    pub shader_uninstall_fn: Option<fn(bool) -> Result<ShaderUninstallResult, String>>,
661
662    /// Callback: check if shader files exist
663    pub shader_has_files_fn: Option<fn(&std::path::Path) -> bool>,
664
665    /// Callback: count shader files
666    pub shader_count_files_fn: Option<fn(&std::path::Path) -> usize>,
667
668    /// Callback: check if shell integration is installed
669    pub shell_integration_is_installed_fn: Option<fn() -> bool>,
670
671    /// Callback: get detected shell type
672    pub shell_integration_detected_shell_fn: Option<fn() -> par_term_config::ShellType>,
673
674    /// Callback: install shell integration
675    pub shell_integration_install_fn: Option<fn() -> Result<ShellIntegrationInstallResult, String>>,
676
677    /// Callback: uninstall shell integration
678    pub shell_integration_uninstall_fn:
679        Option<fn() -> Result<ShellIntegrationUninstallResult, String>>,
680}
681
682impl SettingsUI {
683    /// Create a new settings UI
684    pub fn new(config: Config) -> Self {
685        // Extract values before moving config
686        let initial_cols = config.cols;
687        let initial_rows = config.rows;
688        let initial_collapsed: HashSet<String> =
689            config.collapsed_settings_sections.iter().cloned().collect();
690
691        Self {
692            visible: false,
693            temp_font_bold: config.font_family_bold.clone().unwrap_or_default(),
694            temp_font_italic: config.font_family_italic.clone().unwrap_or_default(),
695            temp_font_bold_italic: config.font_family_bold_italic.clone().unwrap_or_default(),
696            temp_font_family: config.font_family.clone(),
697            temp_font_size: config.font_size,
698            temp_line_spacing: config.line_spacing,
699            temp_char_spacing: config.char_spacing,
700            temp_enable_text_shaping: config.enable_text_shaping,
701            temp_enable_ligatures: config.enable_ligatures,
702            temp_enable_kerning: config.enable_kerning,
703            font_pending_changes: false,
704            temp_custom_shell: config.custom_shell.clone().unwrap_or_default(),
705            temp_shell_args: config
706                .shell_args
707                .as_ref()
708                .map(|args| args.join(" "))
709                .unwrap_or_default(),
710            temp_working_directory: config.working_directory.clone().unwrap_or_default(),
711            temp_startup_directory: config.startup_directory.clone().unwrap_or_default(),
712            temp_initial_text: config.initial_text.clone(),
713            temp_background_image: config.background_image.clone().unwrap_or_default(),
714            temp_custom_shader: config.custom_shader.clone().unwrap_or_default(),
715            temp_cursor_shader: config.cursor_shader.clone().unwrap_or_default(),
716            temp_shader_channel0: config.custom_shader_channel0.clone().unwrap_or_default(),
717            temp_shader_channel1: config.custom_shader_channel1.clone().unwrap_or_default(),
718            temp_shader_channel2: config.custom_shader_channel2.clone().unwrap_or_default(),
719            temp_shader_channel3: config.custom_shader_channel3.clone().unwrap_or_default(),
720            temp_cubemap_path: config.custom_shader_cubemap.clone().unwrap_or_default(),
721            temp_background_color: config.background_color,
722            temp_pane_bg_path: String::new(),
723            temp_pane_bg_mode: BackgroundImageMode::default(),
724            temp_pane_bg_opacity: 1.0,
725            temp_pane_bg_darken: 0.0,
726            temp_pane_bg_index: None,
727            last_live_opacity: config.window_opacity,
728            current_cols: initial_cols,
729            current_rows: initial_rows,
730            supported_vsync_modes: vec![
731                par_term_config::VsyncMode::Immediate,
732                par_term_config::VsyncMode::Mailbox,
733                par_term_config::VsyncMode::Fifo,
734            ],
735            vsync_warning: None,
736            config,
737            has_changes: false,
738            search_query: String::new(),
739            focus_search: true,
740            shader_editor_visible: false,
741            shader_editor_source: String::new(),
742            shader_editor_error: None,
743            shader_editor_original: String::new(),
744            cursor_shader_editor_visible: false,
745            cursor_shader_editor_source: String::new(),
746            cursor_shader_editor_error: None,
747            cursor_shader_editor_original: String::new(),
748            available_agent_ids: Vec::new(),
749            available_shaders: Self::scan_shaders_folder(),
750            available_cubemaps: Self::scan_cubemaps_folder(),
751            new_shader_name: String::new(),
752            show_create_shader_dialog: false,
753            show_delete_shader_dialog: false,
754            shader_search_query: String::new(),
755            shader_search_matches: Vec::new(),
756            shader_search_current: 0,
757            shader_search_visible: false,
758            shader_metadata_cache: ShaderMetadataCache::with_shaders_dir(
759                par_term_config::Config::shaders_dir(),
760            ),
761            cursor_shader_metadata_cache: CursorShaderMetadataCache::with_shaders_dir(
762                par_term_config::Config::shaders_dir(),
763            ),
764            shader_settings_expanded: true,
765            cursor_shader_settings_expanded: true,
766            keybinding_recording_index: None,
767            keybinding_recorded_combo: None,
768            test_notification_requested: false,
769            selected_tab: SettingsTab::default(),
770            collapsed_sections: initial_collapsed,
771            shell_integration_action: None,
772            profile_modal_ui: ProfileModalUI::new(),
773            profile_save_requested: false,
774            profile_open_requested: None,
775            shader_installing: false,
776            shader_status: None,
777            shader_error: None,
778            shader_overwrite_prompt_visible: false,
779            shader_conflicts: Vec::new(),
780            shader_install_receiver: None,
781            editing_trigger_index: None,
782            temp_trigger_name: String::new(),
783            temp_trigger_pattern: String::new(),
784            temp_trigger_actions: Vec::new(),
785            adding_new_trigger: false,
786            trigger_pattern_error: None,
787            editing_coprocess_index: None,
788            temp_coprocess_name: String::new(),
789            temp_coprocess_command: String::new(),
790            temp_coprocess_args: String::new(),
791            temp_coprocess_auto_start: false,
792            temp_coprocess_copy_output: true,
793            temp_coprocess_restart_policy: par_term_config::automation::RestartPolicy::Never,
794            temp_coprocess_restart_delay_ms: 0,
795            adding_new_coprocess: false,
796            trigger_resync_requested: false,
797            pending_coprocess_actions: Vec::new(),
798            coprocess_running: Vec::new(),
799            coprocess_errors: Vec::new(),
800            coprocess_output: Vec::new(),
801            coprocess_output_expanded: Vec::new(),
802            editing_script_index: None,
803            temp_script_name: String::new(),
804            temp_script_path: String::new(),
805            temp_script_args: String::new(),
806            temp_script_auto_start: false,
807            temp_script_enabled: true,
808            temp_script_restart_policy: par_term_config::automation::RestartPolicy::Never,
809            temp_script_restart_delay_ms: 0,
810            temp_script_subscriptions: String::new(),
811            adding_new_script: false,
812            pending_script_actions: Vec::new(),
813            script_running: Vec::new(),
814            script_errors: Vec::new(),
815            script_output: Vec::new(),
816            script_output_expanded: Vec::new(),
817            script_panels: Vec::new(),
818            open_log_requested: false,
819            identify_panes_requested: false,
820            update_install_requested: false,
821            check_now_requested: false,
822            update_status: None,
823            update_result: None,
824            last_update_result: None,
825            update_installing: false,
826            update_install_receiver: None,
827            editing_snippet_index: None,
828            temp_snippet_id: String::new(),
829            temp_snippet_title: String::new(),
830            temp_snippet_content: String::new(),
831            temp_snippet_keybinding: String::new(),
832            temp_snippet_folder: String::new(),
833            temp_snippet_description: String::new(),
834            temp_snippet_keybinding_enabled: true,
835            temp_snippet_auto_execute: false,
836            temp_snippet_variables: Vec::new(),
837            adding_new_snippet: false,
838            editing_action_index: None,
839            temp_action_type: 0,
840            temp_action_id: String::new(),
841            temp_action_title: String::new(),
842            temp_action_command: String::new(),
843            temp_action_args: String::new(),
844            temp_action_text: String::new(),
845            temp_action_keys: String::new(),
846            temp_action_keybinding: String::new(),
847            adding_new_action: false,
848            recording_snippet_keybinding: false,
849            snippet_recorded_combo: None,
850            recording_action_keybinding: false,
851            action_recorded_combo: None,
852            dynamic_source_editing: None,
853            dynamic_source_edit_buffer: None,
854            dynamic_source_new_header_key: String::new(),
855            dynamic_source_new_header_value: String::new(),
856            temp_import_url: String::new(),
857            import_export_status: None,
858            import_export_is_error: false,
859            show_reset_defaults_dialog: false,
860            arrangement_save_name: String::new(),
861            arrangement_confirm_restore: None,
862            arrangement_confirm_delete: None,
863            arrangement_confirm_overwrite: None,
864            arrangement_rename_id: None,
865            arrangement_rename_text: String::new(),
866            pending_arrangement_actions: Vec::new(),
867            arrangement_manager: ArrangementManager::new(),
868            app_version: "",
869            installation_type: InstallationType::StandaloneBinary,
870            shader_install_fn: None,
871            shader_detect_modified_fn: None,
872            shader_uninstall_fn: None,
873            shader_has_files_fn: None,
874            shader_count_files_fn: None,
875            shell_integration_is_installed_fn: None,
876            shell_integration_detected_shell_fn: None,
877            shell_integration_install_fn: None,
878            shell_integration_uninstall_fn: None,
879        }
880    }
881
882    /// Update the current terminal dimensions (called when window resizes)
883    pub fn update_current_size(&mut self, cols: usize, rows: usize) {
884        self.current_cols = cols;
885        self.current_rows = rows;
886    }
887
888    /// Update the list of supported vsync modes (called when renderer is initialized)
889    pub fn update_supported_vsync_modes(&mut self, modes: Vec<par_term_config::VsyncMode>) {
890        self.supported_vsync_modes = modes;
891        self.vsync_warning = None;
892    }
893
894    /// Check if a vsync mode is supported
895    pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
896        self.supported_vsync_modes.contains(&mode)
897    }
898
899    /// Set vsync warning message
900    #[allow(dead_code)]
901    pub fn set_vsync_warning(&mut self, warning: Option<String>) {
902        self.vsync_warning = warning;
903    }
904
905    pub fn pick_file_path(&self, title: &str) -> Option<String> {
906        FileDialog::new()
907            .set_title(title)
908            .pick_file()
909            .map(|p| p.display().to_string())
910    }
911
912    pub fn pick_folder_path(&self, title: &str) -> Option<String> {
913        FileDialog::new()
914            .set_title(title)
915            .pick_folder()
916            .map(|p| p.display().to_string())
917    }
918
919    /// Update the config copy (e.g., when config is reloaded).
920    pub fn update_config(&mut self, config: Config) {
921        if !self.has_changes {
922            self.config = config;
923            self.last_live_opacity = self.config.window_opacity;
924            if !self.font_pending_changes {
925                self.sync_font_temps_from_config();
926            }
927        }
928    }
929
930    /// Force-update the config copy, bypassing the `has_changes` guard.
931    pub fn force_update_config(&mut self, config: Config) {
932        self.config = config;
933        self.sync_all_temps_from_config();
934        self.has_changes = false;
935    }
936
937    fn sync_font_temps_from_config(&mut self) {
938        self.temp_font_family = self.config.font_family.clone();
939        self.temp_font_size = self.config.font_size;
940        self.temp_line_spacing = self.config.line_spacing;
941        self.temp_char_spacing = self.config.char_spacing;
942        self.temp_enable_text_shaping = self.config.enable_text_shaping;
943        self.temp_enable_ligatures = self.config.enable_ligatures;
944        self.temp_enable_kerning = self.config.enable_kerning;
945        self.temp_font_bold = self.config.font_family_bold.clone().unwrap_or_default();
946        self.temp_font_italic = self.config.font_family_italic.clone().unwrap_or_default();
947        self.temp_font_bold_italic = self
948            .config
949            .font_family_bold_italic
950            .clone()
951            .unwrap_or_default();
952        self.font_pending_changes = false;
953    }
954
955    /// Sync ALL temp fields from config
956    pub fn sync_all_temps_from_config(&mut self) {
957        self.sync_font_temps_from_config();
958        self.temp_custom_shell = self.config.custom_shell.clone().unwrap_or_default();
959        self.temp_shell_args = self
960            .config
961            .shell_args
962            .as_ref()
963            .map(|args| args.join(" "))
964            .unwrap_or_default();
965        self.temp_working_directory = self.config.working_directory.clone().unwrap_or_default();
966        self.temp_startup_directory = self.config.startup_directory.clone().unwrap_or_default();
967        self.temp_initial_text = self.config.initial_text.clone();
968        self.temp_background_image = self.config.background_image.clone().unwrap_or_default();
969        self.temp_background_color = self.config.background_color;
970        self.temp_custom_shader = self.config.custom_shader.clone().unwrap_or_default();
971        self.temp_cursor_shader = self.config.cursor_shader.clone().unwrap_or_default();
972        self.temp_shader_channel0 = self
973            .config
974            .custom_shader_channel0
975            .clone()
976            .unwrap_or_default();
977        self.temp_shader_channel1 = self
978            .config
979            .custom_shader_channel1
980            .clone()
981            .unwrap_or_default();
982        self.temp_shader_channel2 = self
983            .config
984            .custom_shader_channel2
985            .clone()
986            .unwrap_or_default();
987        self.temp_shader_channel3 = self
988            .config
989            .custom_shader_channel3
990            .clone()
991            .unwrap_or_default();
992        self.temp_cubemap_path = self
993            .config
994            .custom_shader_cubemap
995            .clone()
996            .unwrap_or_default();
997        self.last_live_opacity = self.config.window_opacity;
998    }
999
1000    /// Reset all settings to their default values
1001    fn reset_all_to_defaults(&mut self) {
1002        self.config = Config::default();
1003        self.sync_all_temps_from_config();
1004        self.has_changes = true;
1005        self.search_query.clear();
1006    }
1007
1008    /// Show the reset to defaults confirmation dialog
1009    fn show_reset_defaults_dialog_window(&mut self, ctx: &Context) {
1010        if !self.show_reset_defaults_dialog {
1011            return;
1012        }
1013
1014        let mut close_dialog = false;
1015        let mut do_reset = false;
1016
1017        egui::Window::new("Reset to Defaults")
1018            .collapsible(false)
1019            .resizable(false)
1020            .order(egui::Order::Foreground)
1021            .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
1022            .show(ctx, |ui| {
1023                ui.vertical_centered(|ui| {
1024                    ui.add_space(10.0);
1025                    ui.label(
1026                        egui::RichText::new("âš  Warning")
1027                            .color(egui::Color32::YELLOW)
1028                            .size(18.0)
1029                            .strong(),
1030                    );
1031                    ui.add_space(10.0);
1032                    ui.label("This will reset ALL settings to their default values.");
1033                    ui.add_space(5.0);
1034                    ui.label(
1035                        egui::RichText::new("Unsaved changes will be lost. This cannot be undone.")
1036                            .color(egui::Color32::GRAY),
1037                    );
1038                    ui.add_space(15.0);
1039
1040                    ui.horizontal(|ui| {
1041                        let reset_button = egui::Button::new(
1042                            egui::RichText::new("Reset").color(egui::Color32::WHITE),
1043                        )
1044                        .fill(egui::Color32::from_rgb(180, 50, 50));
1045
1046                        if ui.add(reset_button).clicked() {
1047                            do_reset = true;
1048                            close_dialog = true;
1049                        }
1050
1051                        ui.add_space(10.0);
1052
1053                        if ui.button("Cancel").clicked() {
1054                            close_dialog = true;
1055                        }
1056                    });
1057                    ui.add_space(10.0);
1058                });
1059            });
1060
1061        if do_reset {
1062            self.reset_all_to_defaults();
1063        }
1064
1065        if close_dialog {
1066            self.show_reset_defaults_dialog = false;
1067        }
1068    }
1069
1070    /// Begin shader install asynchronously with optional force overwrite.
1071    /// The caller must provide a function that performs the actual installation.
1072    pub fn start_shader_install_with<F>(&mut self, force_overwrite: bool, install_fn: F)
1073    where
1074        F: FnOnce(bool) -> Result<ShaderInstallResult, String> + Send + 'static,
1075    {
1076        use std::sync::mpsc;
1077
1078        if self.shader_installing {
1079            return;
1080        }
1081
1082        self.shader_error = None;
1083        self.shader_status = Some(if force_overwrite {
1084            "Reinstalling shaders (overwriting modified files)...".to_string()
1085        } else {
1086            "Reinstalling shaders...".to_string()
1087        });
1088        self.shader_installing = true;
1089
1090        let (tx, rx) = mpsc::channel();
1091        self.shader_install_receiver = Some(rx);
1092
1093        std::thread::spawn(move || {
1094            let result = install_fn(force_overwrite);
1095            let _ = tx.send(result);
1096        });
1097    }
1098
1099    /// Poll for completion of async shader install.
1100    pub fn poll_shader_install_status(&mut self) {
1101        if let Some(receiver) = &self.shader_install_receiver
1102            && let Ok(result) = receiver.try_recv()
1103        {
1104            self.shader_installing = false;
1105            self.shader_install_receiver = None;
1106            match result {
1107                Ok(res) => {
1108                    let detail = if res.skipped > 0 {
1109                        format!(
1110                            "Installed {} shaders ({} skipped, {} removed)",
1111                            res.installed, res.skipped, res.removed
1112                        )
1113                    } else {
1114                        format!(
1115                            "Installed {} shaders ({} removed)",
1116                            res.installed, res.removed
1117                        )
1118                    };
1119                    self.shader_status = Some(detail);
1120                    self.shader_error = None;
1121                    self.config.integration_versions.shaders_installed_version =
1122                        Some(self.app_version.to_string());
1123                }
1124                Err(e) => {
1125                    self.shader_error = Some(e);
1126                    self.shader_status = None;
1127                }
1128            }
1129        }
1130    }
1131
1132    /// Begin self-update asynchronously.
1133    /// The caller must provide a function that performs the actual update.
1134    pub fn start_self_update_with<F>(&mut self, version: String, update_fn: F)
1135    where
1136        F: FnOnce(&str) -> Result<UpdateResult, String> + Send + 'static,
1137    {
1138        use std::sync::mpsc;
1139
1140        if self.update_installing {
1141            return;
1142        }
1143
1144        self.update_status = Some("Downloading and installing update...".to_string());
1145        self.update_result = None;
1146        self.update_installing = true;
1147
1148        let (tx, rx) = mpsc::channel();
1149        self.update_install_receiver = Some(rx);
1150
1151        std::thread::spawn(move || {
1152            let result = update_fn(&version);
1153            let _ = tx.send(result);
1154        });
1155    }
1156
1157    /// Poll for completion of async self-update.
1158    pub fn poll_update_install_status(&mut self) {
1159        if let Some(receiver) = &self.update_install_receiver
1160            && let Ok(result) = receiver.try_recv()
1161        {
1162            self.update_installing = false;
1163            self.update_install_receiver = None;
1164            match &result {
1165                Ok(res) => {
1166                    self.update_status = Some(format!(
1167                        "Update installed! Restart par-term to use v{}",
1168                        res.new_version
1169                    ));
1170                }
1171                Err(e) => {
1172                    self.update_status = Some(format!("Update failed: {}", e));
1173                }
1174            }
1175            self.update_result = Some(result);
1176        }
1177    }
1178
1179    /// Apply font changes from temp variables to config
1180    pub fn apply_font_changes(&mut self) {
1181        self.config.font_family = self.temp_font_family.clone();
1182        self.config.font_size = self.temp_font_size;
1183        self.config.line_spacing = self.temp_line_spacing;
1184        self.config.char_spacing = self.temp_char_spacing;
1185        self.config.enable_text_shaping = self.temp_enable_text_shaping;
1186        self.config.enable_ligatures = self.temp_enable_ligatures;
1187        self.config.enable_kerning = self.temp_enable_kerning;
1188        self.config.font_family_bold = if self.temp_font_bold.is_empty() {
1189            None
1190        } else {
1191            Some(self.temp_font_bold.clone())
1192        };
1193        self.config.font_family_italic = if self.temp_font_italic.is_empty() {
1194            None
1195        } else {
1196            Some(self.temp_font_italic.clone())
1197        };
1198        self.config.font_family_bold_italic = if self.temp_font_bold_italic.is_empty() {
1199            None
1200        } else {
1201            Some(self.temp_font_bold_italic.clone())
1202        };
1203        self.font_pending_changes = false;
1204    }
1205
1206    /// Toggle settings window visibility
1207    #[allow(dead_code)]
1208    pub fn toggle(&mut self) {
1209        self.visible = !self.visible;
1210        if self.visible {
1211            self.focus_search = true;
1212        }
1213    }
1214
1215    /// Get a reference to the working config (for live sync)
1216    #[allow(dead_code)]
1217    pub fn current_config(&self) -> &Config {
1218        &self.config
1219    }
1220
1221    /// Sync the current collapsed sections state into the config's persisted field.
1222    fn sync_collapsed_sections_to_config(&mut self) {
1223        self.config.collapsed_settings_sections = self.collapsed_sections.iter().cloned().collect();
1224    }
1225
1226    /// Get a snapshot of the current collapsed section IDs for persistence on close.
1227    pub fn collapsed_sections_snapshot(&self) -> Vec<String> {
1228        self.collapsed_sections.iter().cloned().collect()
1229    }
1230
1231    /// Check if a test notification was requested and clear the flag
1232    pub fn take_test_notification_request(&mut self) -> bool {
1233        let requested = self.test_notification_requested;
1234        self.test_notification_requested = false;
1235        requested
1236    }
1237
1238    /// Sync profiles from the main window's profile manager into the inline editor.
1239    pub fn sync_profiles(&mut self, profiles: Vec<Profile>) {
1240        self.profile_modal_ui.load_profiles(profiles);
1241    }
1242
1243    /// Take profile save request: returns working profiles if save was requested.
1244    pub fn take_profile_save_request(&mut self) -> Option<Vec<Profile>> {
1245        if self.profile_save_requested {
1246            self.profile_save_requested = false;
1247            Some(self.profile_modal_ui.get_working_profiles().to_vec())
1248        } else {
1249            None
1250        }
1251    }
1252
1253    /// Take profile open request: returns and clears the profile ID to open.
1254    pub fn take_profile_open_request(&mut self) -> Option<ProfileId> {
1255        self.profile_open_requested.take()
1256    }
1257
1258    /// Show the settings window and return results
1259    #[allow(dead_code)]
1260    pub fn show(
1261        &mut self,
1262        ctx: &Context,
1263    ) -> (
1264        Option<Config>,
1265        Option<Config>,
1266        Option<ShaderEditorResult>,
1267        Option<CursorShaderEditorResult>,
1268    ) {
1269        if !self.visible && !self.shader_editor_visible && !self.cursor_shader_editor_visible {
1270            return (None, None, None, None);
1271        }
1272
1273        log::info!("SettingsUI.show() called - visible: true");
1274
1275        if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
1276            if self.cursor_shader_editor_visible {
1277                self.cursor_shader_editor_visible = false;
1278                self.cursor_shader_editor_error = None;
1279            } else if self.shader_editor_visible {
1280                self.shader_editor_visible = false;
1281                self.shader_editor_error = None;
1282            } else if self.visible {
1283                self.visible = false;
1284                return (None, None, None, None);
1285            }
1286        }
1287
1288        let mut style = (*ctx.style()).clone();
1289        let solid_bg = Color32::from_rgba_unmultiplied(24, 24, 24, 255);
1290        style.visuals.window_fill = solid_bg;
1291        style.visuals.panel_fill = solid_bg;
1292        style.visuals.widgets.noninteractive.bg_fill = solid_bg;
1293        ctx.set_style(style);
1294
1295        let mut save_requested = false;
1296        let mut discard_requested = false;
1297        let mut close_requested = false;
1298        let mut open = true;
1299        let mut changes_this_frame = false;
1300
1301        if self.visible {
1302            let settings_viewport = ctx.input(|i| i.viewport_rect());
1303            Window::new("Settings")
1304                .resizable(true)
1305                .default_width(650.0)
1306                .default_height(700.0)
1307                .default_pos(settings_viewport.center())
1308                .pivot(egui::Align2::CENTER_CENTER)
1309                .open(&mut open)
1310                .frame(
1311                    Frame::window(&ctx.style())
1312                        .fill(solid_bg)
1313                        .stroke(egui::Stroke::NONE)
1314                        .shadow(Shadow {
1315                            offset: [0, 0],
1316                            blur: 0,
1317                            spread: 0,
1318                            color: Color32::TRANSPARENT,
1319                        }),
1320                )
1321                .show(ctx, |ui| {
1322                    // Fixed header area (never scrolls)
1323                    ui.heading("Terminal Settings");
1324                    ui.horizontal(|ui| {
1325                        ui.label("Quick search:");
1326                        let response = ui.add(
1327                            egui::TextEdit::singleline(&mut self.search_query)
1328                                .hint_text("Type to filter settings"),
1329                        );
1330                        if self.focus_search {
1331                            self.focus_search = false;
1332                            response.request_focus();
1333                        }
1334                    });
1335                    ui.separator();
1336
1337                    // Settings sections (sidebar + content) fill remaining space
1338                    // Each has its own scroll area internally
1339                    self.show_settings_sections(ui, &mut changes_this_frame);
1340
1341                    // Footer
1342                    ui.separator();
1343                    ui.horizontal(|ui| {
1344                        if ui.button("Save").clicked() {
1345                            save_requested = true;
1346                        }
1347                        if ui.button("Discard").clicked() {
1348                            discard_requested = true;
1349                        }
1350                        if ui.button("Close").clicked() {
1351                            close_requested = true;
1352                        }
1353                        ui.separator();
1354                        if ui
1355                            .button("Edit Config File")
1356                            .on_hover_text("Open config.yaml in your default editor")
1357                            .clicked()
1358                        {
1359                            let config_path = Config::config_path();
1360                            if let Err(e) = open::that(&config_path) {
1361                                log::error!("Failed to open config file: {}", e);
1362                            }
1363                        }
1364                        if ui
1365                            .button("Reset to Defaults")
1366                            .on_hover_text("Reset all settings to their default values")
1367                            .clicked()
1368                        {
1369                            self.show_reset_defaults_dialog = true;
1370                        }
1371                        if self.has_changes {
1372                            ui.colored_label(egui::Color32::YELLOW, "* Unsaved changes");
1373                        }
1374                    });
1375                });
1376        }
1377
1378        let shader_apply_result = self.show_shader_editor_window(ctx);
1379        let cursor_shader_apply_result = self.show_cursor_shader_editor_window(ctx);
1380
1381        self.show_create_shader_dialog_window(ctx);
1382        self.show_delete_shader_dialog_window(ctx);
1383        self.show_reset_defaults_dialog_window(ctx);
1384
1385        if self.visible && (!open || close_requested) {
1386            self.visible = false;
1387        }
1388
1389        let config_to_save = if save_requested {
1390            if self.font_pending_changes {
1391                self.apply_font_changes();
1392            }
1393            self.has_changes = false;
1394            self.sync_collapsed_sections_to_config();
1395            let mut config = self.config.clone();
1396            config.generate_snippet_action_keybindings();
1397            Some(config)
1398        } else {
1399            None
1400        };
1401
1402        if discard_requested {
1403            self.has_changes = false;
1404            self.sync_font_temps_from_config();
1405        }
1406
1407        let config_for_live_update = if self.visible {
1408            if (self.config.window_opacity - self.last_live_opacity).abs() > f32::EPSILON {
1409                log::info!(
1410                    "SettingsUI: live opacity {:.3} (last {:.3})",
1411                    self.config.window_opacity,
1412                    self.last_live_opacity
1413                );
1414                self.last_live_opacity = self.config.window_opacity;
1415            }
1416            Some(self.config.clone())
1417        } else {
1418            None
1419        };
1420
1421        (
1422            config_to_save,
1423            config_for_live_update,
1424            shader_apply_result,
1425            cursor_shader_apply_result,
1426        )
1427    }
1428
1429    /// Show the settings UI as a full-window panel (for standalone settings window)
1430    pub fn show_as_panel(
1431        &mut self,
1432        ctx: &Context,
1433    ) -> (
1434        Option<Config>,
1435        Option<Config>,
1436        Option<ShaderEditorResult>,
1437        Option<CursorShaderEditorResult>,
1438    ) {
1439        if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
1440            if self.cursor_shader_editor_visible {
1441                self.cursor_shader_editor_visible = false;
1442                self.cursor_shader_editor_error = None;
1443            } else if self.shader_editor_visible {
1444                self.shader_editor_visible = false;
1445                self.shader_editor_error = None;
1446            }
1447        }
1448
1449        let mut style = (*ctx.style()).clone();
1450        let solid_bg = Color32::from_rgba_unmultiplied(24, 24, 24, 255);
1451        style.visuals.window_fill = solid_bg;
1452        style.visuals.panel_fill = solid_bg;
1453        style.visuals.widgets.noninteractive.bg_fill = solid_bg;
1454        ctx.set_style(style);
1455
1456        let mut save_requested = false;
1457        let mut discard_requested = false;
1458        let mut changes_this_frame = false;
1459
1460        egui::CentralPanel::default()
1461            .frame(Frame::central_panel(&ctx.style()).fill(solid_bg))
1462            .show(ctx, |ui| {
1463                // Fixed header area (never scrolls)
1464                ui.heading("Terminal Settings");
1465                ui.horizontal(|ui| {
1466                    ui.label("Quick search:");
1467                    let response = ui.add(
1468                        egui::TextEdit::singleline(&mut self.search_query)
1469                            .hint_text("Type to filter settings"),
1470                    );
1471                    if self.focus_search {
1472                        self.focus_search = false;
1473                        response.request_focus();
1474                    }
1475                });
1476                ui.separator();
1477
1478                // Settings sections (sidebar + content) fill remaining space
1479                // Each has its own scroll area internally
1480                self.show_settings_sections(ui, &mut changes_this_frame);
1481
1482                // Footer
1483                ui.separator();
1484                ui.horizontal(|ui| {
1485                    if ui.button("Save").clicked() {
1486                        save_requested = true;
1487                    }
1488                    if ui.button("Discard").clicked() {
1489                        discard_requested = true;
1490                    }
1491                    ui.separator();
1492                    if ui
1493                        .button("Edit Config File")
1494                        .on_hover_text("Open config.yaml in your default editor")
1495                        .clicked()
1496                    {
1497                        let config_path = Config::config_path();
1498                        if let Err(e) = open::that(&config_path) {
1499                            log::error!("Failed to open config file: {}", e);
1500                        }
1501                    }
1502                    if ui
1503                        .button("Reset to Defaults")
1504                        .on_hover_text("Reset all settings to their default values")
1505                        .clicked()
1506                    {
1507                        self.show_reset_defaults_dialog = true;
1508                    }
1509                    if self.has_changes {
1510                        ui.colored_label(egui::Color32::YELLOW, "* Unsaved changes");
1511                    }
1512                });
1513            });
1514
1515        let shader_apply_result = self.show_shader_editor_window(ctx);
1516        let cursor_shader_apply_result = self.show_cursor_shader_editor_window(ctx);
1517
1518        self.show_create_shader_dialog_window(ctx);
1519        self.show_delete_shader_dialog_window(ctx);
1520        self.show_reset_defaults_dialog_window(ctx);
1521
1522        let config_to_save = if save_requested {
1523            if self.font_pending_changes {
1524                self.apply_font_changes();
1525            }
1526            self.has_changes = false;
1527            self.sync_collapsed_sections_to_config();
1528            let mut config = self.config.clone();
1529            config.generate_snippet_action_keybindings();
1530            Some(config)
1531        } else {
1532            None
1533        };
1534
1535        if discard_requested {
1536            self.has_changes = false;
1537            self.sync_font_temps_from_config();
1538        }
1539
1540        let config_for_live_update = {
1541            if (self.config.window_opacity - self.last_live_opacity).abs() > f32::EPSILON {
1542                log::info!(
1543                    "SettingsUI: live opacity {:.3} (last {:.3})",
1544                    self.config.window_opacity,
1545                    self.last_live_opacity
1546                );
1547                self.last_live_opacity = self.config.window_opacity;
1548            }
1549            Some(self.config.clone())
1550        };
1551
1552        (
1553            config_to_save,
1554            config_for_live_update,
1555            shader_apply_result,
1556            cursor_shader_apply_result,
1557        )
1558    }
1559
1560    /// Show all settings sections using the new sidebar + tab layout.
1561    fn show_settings_sections(&mut self, ui: &mut egui::Ui, changes_this_frame: &mut bool) {
1562        quick_settings::show(ui, self, changes_this_frame);
1563        ui.separator();
1564
1565        let available_width = ui.available_width();
1566        // Reserve space for the footer (separator + button row)
1567        let footer_height = 45.0;
1568        let available_height = (ui.available_height() - footer_height).max(100.0);
1569        let sidebar_width = 150.0;
1570        let content_width = (available_width - sidebar_width - 15.0).max(300.0);
1571
1572        let layout = egui::Layout::left_to_right(egui::Align::Min);
1573        ui.allocate_ui_with_layout(
1574            egui::vec2(available_width, available_height),
1575            layout,
1576            |ui| {
1577                // Sidebar with its own scroll area
1578                ui.allocate_ui_with_layout(
1579                    egui::vec2(sidebar_width, available_height),
1580                    egui::Layout::top_down(egui::Align::Min),
1581                    |ui| {
1582                        egui::ScrollArea::vertical()
1583                            .id_salt("settings_sidebar")
1584                            .max_height(available_height)
1585                            .show(ui, |ui| {
1586                                sidebar::show(ui, &mut self.selected_tab, &self.search_query);
1587                            });
1588                    },
1589                );
1590
1591                ui.separator();
1592
1593                // Content area with its own scroll area
1594                ui.allocate_ui_with_layout(
1595                    egui::vec2(content_width, available_height),
1596                    egui::Layout::top_down(egui::Align::Min),
1597                    |ui| {
1598                        egui::ScrollArea::vertical()
1599                            .id_salt("settings_tab_content")
1600                            .max_height(available_height)
1601                            .show(ui, |ui| {
1602                                ui.set_min_width(content_width - 20.0);
1603                                self.show_tab_content(ui, changes_this_frame);
1604                            });
1605                    },
1606                );
1607            },
1608        );
1609    }
1610
1611    /// Show the content for the currently selected tab.
1612    fn show_tab_content(&mut self, ui: &mut egui::Ui, changes_this_frame: &mut bool) {
1613        let mut collapsed = std::mem::take(&mut self.collapsed_sections);
1614
1615        match self.selected_tab {
1616            SettingsTab::Appearance => {
1617                appearance_tab::show(ui, self, changes_this_frame, &mut collapsed);
1618            }
1619            SettingsTab::Window => {
1620                window_tab::show(ui, self, changes_this_frame, &mut collapsed);
1621            }
1622            SettingsTab::Input => {
1623                input_tab::show(ui, self, changes_this_frame, &mut collapsed);
1624            }
1625            SettingsTab::Terminal => {
1626                terminal_tab::show(ui, self, changes_this_frame, &mut collapsed);
1627            }
1628            SettingsTab::Effects => {
1629                effects_tab::show(ui, self, changes_this_frame, &mut collapsed);
1630            }
1631            SettingsTab::Badge => {
1632                badge_tab::show(ui, self, changes_this_frame, &mut collapsed);
1633            }
1634            SettingsTab::ProgressBar => {
1635                progress_bar_tab::show(ui, self, changes_this_frame, &mut collapsed);
1636            }
1637            SettingsTab::StatusBar => {
1638                status_bar_tab::show(ui, self, changes_this_frame, &mut collapsed);
1639            }
1640            SettingsTab::Profiles => {
1641                profiles_tab::show(ui, self, changes_this_frame, &mut collapsed);
1642            }
1643            SettingsTab::Ssh => {
1644                self.show_ssh_tab(ui, changes_this_frame);
1645            }
1646            SettingsTab::Notifications => {
1647                notifications_tab::show(ui, self, changes_this_frame, &mut collapsed);
1648            }
1649            SettingsTab::Integrations => {
1650                self.show_integrations_tab(ui, changes_this_frame, &mut collapsed);
1651            }
1652            SettingsTab::Automation => {
1653                automation_tab::show(ui, self, changes_this_frame, &mut collapsed);
1654            }
1655            SettingsTab::Scripts => {
1656                scripts_tab::show(ui, self, changes_this_frame, &mut collapsed);
1657            }
1658            SettingsTab::Snippets => {
1659                snippets_tab::show(ui, self, changes_this_frame, &mut collapsed);
1660            }
1661            SettingsTab::Actions => {
1662                actions_tab::show(ui, self, changes_this_frame, &mut collapsed);
1663            }
1664            SettingsTab::Arrangements => {
1665                arrangements_tab::show(ui, self, changes_this_frame, &mut collapsed);
1666            }
1667            SettingsTab::AiInspector => {
1668                ai_inspector_tab::show(ui, self, changes_this_frame, &mut collapsed);
1669            }
1670            SettingsTab::Advanced => {
1671                advanced_tab::show(ui, self, changes_this_frame, &mut collapsed);
1672            }
1673        }
1674
1675        self.collapsed_sections = collapsed;
1676    }
1677
1678    /// Check if a keybinding conflicts with existing keybindings.
1679    pub fn check_keybinding_conflict(&self, key: &str, exclude_id: Option<&str>) -> Option<String> {
1680        for binding in &self.config.keybindings {
1681            if binding.key == key {
1682                return Some(format!("Already bound to: {}", binding.action));
1683            }
1684        }
1685
1686        for snippet in &self.config.snippets {
1687            if let Some(snippet_key) = &snippet.keybinding
1688                && snippet_key == key
1689            {
1690                if exclude_id == Some(&snippet.id) {
1691                    continue;
1692                }
1693                return Some(format!("Already bound to snippet: {}", snippet.title));
1694            }
1695        }
1696
1697        None
1698    }
1699}