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