1use 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
15pub type ShaderDetectModifiedFn = fn() -> Result<Vec<String>, String>;
17
18mod traits;
20pub use traits::*;
21
22pub mod arrangements;
24pub use arrangements::{
25 ArrangementId, ArrangementManager, MonitorInfo, TabSnapshot, WindowArrangement, WindowSnapshot,
26};
27
28pub mod shell_detection;
30
31pub mod profile_modal_ui;
33pub use profile_modal_ui::{ProfileModalAction, ProfileModalUI};
34
35pub 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
59pub mod background_tab;
61
62mod cursor_shader_editor;
64mod shader_dialogs;
65mod shader_editor;
66mod shader_utils;
67
68pub use sidebar::SettingsTab;
69
70pub 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#[derive(Debug, Clone)]
78pub struct ShaderEditorResult {
79 pub source: String,
81}
82
83#[derive(Debug, Clone)]
85pub struct CursorShaderEditorResult {
86 pub source: String,
88}
89
90#[derive(Debug, Clone)]
96pub enum SettingsWindowAction {
97 None,
99 Close,
101 ApplyConfig(Config),
103 SaveConfig(Config),
105 ApplyShader(ShaderEditorResult),
107 ApplyCursorShader(CursorShaderEditorResult),
109 TestNotification,
111 SaveProfiles(Vec<Profile>),
113 OpenProfile(ProfileId),
115 StartCoprocess(usize),
117 StopCoprocess(usize),
119 StartScript(usize),
121 StopScript(usize),
123 OpenLogFile,
125 SaveArrangement(String),
127 RestoreArrangement(ArrangementId),
129 DeleteArrangement(ArrangementId),
131 RenameArrangement(ArrangementId, String),
133 ForceUpdateCheck,
135 InstallUpdate(String),
137 IdentifyPanes,
139 InstallShellIntegration,
141 UninstallShellIntegration,
143}
144
145#[derive(Debug, Clone)]
147pub struct ArrangementInfo {
148 pub id: ArrangementId,
150 pub name: String,
152 pub window_count: usize,
154}
155
156#[derive(Debug, Clone)]
158pub struct ShaderInstallResult {
159 pub installed: usize,
161 pub skipped: usize,
163 pub removed: usize,
165}
166
167#[derive(Debug, Clone)]
169pub struct ShaderUninstallResult {
170 pub removed: usize,
172 pub kept: usize,
174 pub needs_confirmation: bool,
176}
177
178#[derive(Debug, Clone)]
180pub struct ShellIntegrationInstallResult {
181 pub shell: String,
183 pub script_path: String,
185 pub rc_file: String,
187 pub needs_restart: bool,
189}
190
191#[derive(Debug, Clone)]
193pub struct ShellIntegrationUninstallResult {
194 pub cleaned: bool,
196 pub needs_manual: bool,
198 pub scripts_removed: usize,
200}
201
202#[derive(Debug, Clone)]
204pub struct UpdateResult {
205 pub old_version: String,
207 pub new_version: String,
209 pub install_path: String,
211 pub needs_restart: bool,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum InstallationType {
218 Homebrew,
220 CargoInstall,
222 MacOSBundle,
224 StandaloneBinary,
226}
227
228#[derive(Debug, Clone)]
230pub enum UpdateCheckResult {
231 UpToDate,
233 UpdateAvailable(UpdateCheckInfo),
235 Disabled,
237 Skipped,
239 Error(String),
241}
242
243#[derive(Debug, Clone)]
245pub struct UpdateCheckInfo {
246 pub version: String,
248 pub release_notes: Option<String>,
250 pub release_url: String,
252 pub published_at: Option<String>,
254}
255
256pub 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
264pub 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
276pub fn http_agent() -> ureq::Agent {
278 ureq::Agent::new_with_defaults()
279}
280
281pub struct SettingsUI {
283 pub visible: bool,
285
286 pub config: Config,
288
289 pub last_live_opacity: f32,
291
292 pub has_changes: bool,
294
295 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 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 pub temp_cubemap_path: String,
324
325 pub temp_background_color: [u8; 3],
327
328 pub temp_pane_bg_path: String,
330 pub temp_pane_bg_mode: BackgroundImageMode,
332 pub temp_pane_bg_opacity: f32,
334 pub temp_pane_bg_index: Option<usize>,
336
337 pub search_query: String,
339 pub focus_search: bool,
341
342 pub shader_editor_visible: bool,
345 pub shader_editor_source: String,
347 pub shader_editor_error: Option<String>,
349 pub shader_editor_original: String,
351
352 pub cursor_shader_editor_visible: bool,
355 pub cursor_shader_editor_source: String,
357 pub cursor_shader_editor_error: Option<String>,
359 pub cursor_shader_editor_original: String,
361
362 pub available_agent_ids: Vec<(String, String)>,
365
366 pub available_shaders: Vec<String>,
369 pub available_cubemaps: Vec<String>,
371 pub new_shader_name: String,
373 pub show_create_shader_dialog: bool,
375 pub show_delete_shader_dialog: bool,
377
378 pub shader_search_query: String,
381 pub shader_search_matches: Vec<usize>,
383 pub shader_search_current: usize,
385 pub shader_search_visible: bool,
387
388 pub shader_metadata_cache: ShaderMetadataCache,
391 pub cursor_shader_metadata_cache: CursorShaderMetadataCache,
393 pub shader_settings_expanded: bool,
395 pub cursor_shader_settings_expanded: bool,
397
398 pub current_cols: usize,
401 pub current_rows: usize,
403
404 pub supported_vsync_modes: Vec<par_term_config::VsyncMode>,
407 pub vsync_warning: Option<String>,
409
410 pub keybinding_recording_index: Option<usize>,
413 pub keybinding_recorded_combo: Option<String>,
415
416 pub test_notification_requested: bool,
419
420 pub selected_tab: SettingsTab,
423 pub collapsed_sections: HashSet<String>,
425
426 pub shell_integration_action: Option<integrations_tab::ShellIntegrationAction>,
429
430 pub profile_modal_ui: ProfileModalUI,
433 pub profile_save_requested: bool,
435 pub profile_open_requested: Option<ProfileId>,
437 shader_installing: bool,
440 shader_status: Option<String>,
442 shader_error: Option<String>,
444 shader_overwrite_prompt_visible: bool,
446 shader_conflicts: Vec<String>,
448 shader_install_receiver: Option<std::sync::mpsc::Receiver<Result<ShaderInstallResult, String>>>,
450
451 pub editing_trigger_index: Option<usize>,
454 pub temp_trigger_name: String,
456 pub temp_trigger_pattern: String,
458 pub temp_trigger_actions: Vec<par_term_config::automation::TriggerActionConfig>,
460 pub adding_new_trigger: bool,
462 pub trigger_pattern_error: Option<String>,
464 pub editing_coprocess_index: Option<usize>,
466 pub temp_coprocess_name: String,
468 pub temp_coprocess_command: String,
470 pub temp_coprocess_args: String,
472 pub temp_coprocess_auto_start: bool,
474 pub temp_coprocess_copy_output: bool,
476 pub temp_coprocess_restart_policy: par_term_config::automation::RestartPolicy,
478 pub temp_coprocess_restart_delay_ms: u64,
480 pub adding_new_coprocess: bool,
482 pub trigger_resync_requested: bool,
484 pub pending_coprocess_actions: Vec<(usize, bool)>,
486 pub coprocess_running: Vec<bool>,
488 pub coprocess_errors: Vec<String>,
490 pub coprocess_output: Vec<Vec<String>>,
492 pub coprocess_output_expanded: Vec<bool>,
494 pub editing_script_index: Option<usize>,
497 pub temp_script_name: String,
499 pub temp_script_path: String,
501 pub temp_script_args: String,
503 pub temp_script_auto_start: bool,
505 pub temp_script_enabled: bool,
507 pub temp_script_restart_policy: par_term_config::automation::RestartPolicy,
509 pub temp_script_restart_delay_ms: u64,
511 pub temp_script_subscriptions: String,
513 pub adding_new_script: bool,
515 pub pending_script_actions: Vec<(usize, bool)>,
517 pub script_running: Vec<bool>,
519 pub script_errors: Vec<String>,
521 pub script_output: Vec<Vec<String>>,
523 pub script_output_expanded: Vec<bool>,
525 pub script_panels: Vec<Option<(String, String)>>,
527
528 pub open_log_requested: bool,
530
531 pub identify_panes_requested: bool,
533
534 pub update_install_requested: bool,
537 pub check_now_requested: bool,
539 pub update_status: Option<String>,
541 pub update_result: Option<Result<UpdateResult, String>>,
543 pub last_update_result: Option<UpdateCheckResult>,
545 pub update_installing: bool,
547 update_install_receiver: Option<std::sync::mpsc::Receiver<Result<UpdateResult, String>>>,
549
550 pub editing_snippet_index: Option<usize>,
553 pub temp_snippet_id: String,
555 pub temp_snippet_title: String,
557 pub temp_snippet_content: String,
559 pub temp_snippet_keybinding: String,
561 pub temp_snippet_folder: String,
563 pub temp_snippet_description: String,
565 pub temp_snippet_keybinding_enabled: bool,
567 pub temp_snippet_auto_execute: bool,
569 pub temp_snippet_variables: Vec<(String, String)>,
571 pub adding_new_snippet: bool,
573 pub recording_snippet_keybinding: bool,
575 pub snippet_recorded_combo: Option<String>,
577
578 pub editing_action_index: Option<usize>,
581 pub temp_action_type: usize,
583 pub temp_action_id: String,
585 pub temp_action_title: String,
587 pub temp_action_command: String,
589 pub temp_action_args: String,
591 pub temp_action_text: String,
593 pub temp_action_keys: String,
595 pub temp_action_keybinding: String,
597 pub adding_new_action: bool,
599 pub recording_action_keybinding: bool,
601 pub action_recorded_combo: Option<String>,
603
604 pub dynamic_source_editing: Option<usize>,
607 pub dynamic_source_edit_buffer: Option<par_term_config::DynamicProfileSource>,
609 pub dynamic_source_new_header_key: String,
611 pub dynamic_source_new_header_value: String,
613
614 pub temp_import_url: String,
617 pub import_export_status: Option<String>,
619 pub import_export_is_error: bool,
621
622 pub show_reset_defaults_dialog: bool,
625
626 pub arrangement_save_name: String,
629 pub arrangement_confirm_restore: Option<ArrangementId>,
631 pub arrangement_confirm_delete: Option<ArrangementId>,
633 pub arrangement_confirm_overwrite: Option<String>,
635 pub arrangement_rename_id: Option<ArrangementId>,
637 pub arrangement_rename_text: String,
639 pub pending_arrangement_actions: Vec<SettingsWindowAction>,
641 pub arrangement_manager: ArrangementManager,
643
644 pub app_version: &'static str,
647
648 pub installation_type: InstallationType,
650
651 pub shader_install_fn: Option<fn(bool) -> Result<ShaderInstallResult, String>>,
653
654 pub shader_detect_modified_fn: Option<ShaderDetectModifiedFn>,
656
657 pub shader_uninstall_fn: Option<fn(bool) -> Result<ShaderUninstallResult, String>>,
659
660 pub shader_has_files_fn: Option<fn(&std::path::Path) -> bool>,
662
663 pub shader_count_files_fn: Option<fn(&std::path::Path) -> usize>,
665
666 pub shell_integration_is_installed_fn: Option<fn() -> bool>,
668
669 pub shell_integration_detected_shell_fn: Option<fn() -> par_term_config::ShellType>,
671
672 pub shell_integration_install_fn: Option<fn() -> Result<ShellIntegrationInstallResult, String>>,
674
675 pub shell_integration_uninstall_fn:
677 Option<fn() -> Result<ShellIntegrationUninstallResult, String>>,
678}
679
680impl SettingsUI {
681 pub fn new(config: Config) -> Self {
683 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 pub fn update_current_size(&mut self, cols: usize, rows: usize) {
881 self.current_cols = cols;
882 self.current_rows = rows;
883 }
884
885 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 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
893 self.supported_vsync_modes.contains(&mode)
894 }
895
896 #[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 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 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 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 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 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 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 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 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 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 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 #[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 #[allow(dead_code)]
1214 pub fn current_config(&self) -> &Config {
1215 &self.config
1216 }
1217
1218 fn sync_collapsed_sections_to_config(&mut self) {
1220 self.config.collapsed_settings_sections = self.collapsed_sections.iter().cloned().collect();
1221 }
1222
1223 pub fn collapsed_sections_snapshot(&self) -> Vec<String> {
1225 self.collapsed_sections.iter().cloned().collect()
1226 }
1227
1228 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 pub fn sync_profiles(&mut self, profiles: Vec<Profile>) {
1237 self.profile_modal_ui.load_profiles(profiles);
1238 }
1239
1240 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 pub fn take_profile_open_request(&mut self) -> Option<ProfileId> {
1252 self.profile_open_requested.take()
1253 }
1254
1255 #[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 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 self.show_settings_sections(ui, &mut changes_this_frame);
1337
1338 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 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 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 self.show_settings_sections(ui, &mut changes_this_frame);
1478
1479 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 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 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 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 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 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 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}