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_darken: f32,
336 pub temp_pane_bg_index: Option<usize>,
338
339 pub search_query: String,
341 pub focus_search: bool,
343
344 pub shader_editor_visible: bool,
347 pub shader_editor_source: String,
349 pub shader_editor_error: Option<String>,
351 pub shader_editor_original: String,
353
354 pub cursor_shader_editor_visible: bool,
357 pub cursor_shader_editor_source: String,
359 pub cursor_shader_editor_error: Option<String>,
361 pub cursor_shader_editor_original: String,
363
364 pub available_agent_ids: Vec<(String, String)>,
367
368 pub available_shaders: Vec<String>,
371 pub available_cubemaps: Vec<String>,
373 pub new_shader_name: String,
375 pub show_create_shader_dialog: bool,
377 pub show_delete_shader_dialog: bool,
379
380 pub shader_search_query: String,
383 pub shader_search_matches: Vec<usize>,
385 pub shader_search_current: usize,
387 pub shader_search_visible: bool,
389
390 pub shader_metadata_cache: ShaderMetadataCache,
393 pub cursor_shader_metadata_cache: CursorShaderMetadataCache,
395 pub shader_settings_expanded: bool,
397 pub cursor_shader_settings_expanded: bool,
399
400 pub current_cols: usize,
403 pub current_rows: usize,
405
406 pub supported_vsync_modes: Vec<par_term_config::VsyncMode>,
409 pub vsync_warning: Option<String>,
411
412 pub keybinding_recording_index: Option<usize>,
415 pub keybinding_recorded_combo: Option<String>,
417
418 pub test_notification_requested: bool,
421
422 pub selected_tab: SettingsTab,
425 pub collapsed_sections: HashSet<String>,
427
428 pub shell_integration_action: Option<integrations_tab::ShellIntegrationAction>,
431
432 pub profile_modal_ui: ProfileModalUI,
435 pub profile_save_requested: bool,
437 pub profile_open_requested: Option<ProfileId>,
439 shader_installing: bool,
442 shader_status: Option<String>,
444 shader_error: Option<String>,
446 shader_overwrite_prompt_visible: bool,
448 shader_conflicts: Vec<String>,
450 shader_install_receiver: Option<std::sync::mpsc::Receiver<Result<ShaderInstallResult, String>>>,
452
453 pub editing_trigger_index: Option<usize>,
456 pub temp_trigger_name: String,
458 pub temp_trigger_pattern: String,
460 pub temp_trigger_actions: Vec<par_term_config::automation::TriggerActionConfig>,
462 pub adding_new_trigger: bool,
464 pub trigger_pattern_error: Option<String>,
466 pub editing_coprocess_index: Option<usize>,
468 pub temp_coprocess_name: String,
470 pub temp_coprocess_command: String,
472 pub temp_coprocess_args: String,
474 pub temp_coprocess_auto_start: bool,
476 pub temp_coprocess_copy_output: bool,
478 pub temp_coprocess_restart_policy: par_term_config::automation::RestartPolicy,
480 pub temp_coprocess_restart_delay_ms: u64,
482 pub adding_new_coprocess: bool,
484 pub trigger_resync_requested: bool,
486 pub pending_coprocess_actions: Vec<(usize, bool)>,
488 pub coprocess_running: Vec<bool>,
490 pub coprocess_errors: Vec<String>,
492 pub coprocess_output: Vec<Vec<String>>,
494 pub coprocess_output_expanded: Vec<bool>,
496 pub editing_script_index: Option<usize>,
499 pub temp_script_name: String,
501 pub temp_script_path: String,
503 pub temp_script_args: String,
505 pub temp_script_auto_start: bool,
507 pub temp_script_enabled: bool,
509 pub temp_script_restart_policy: par_term_config::automation::RestartPolicy,
511 pub temp_script_restart_delay_ms: u64,
513 pub temp_script_subscriptions: String,
515 pub adding_new_script: bool,
517 pub pending_script_actions: Vec<(usize, bool)>,
519 pub script_running: Vec<bool>,
521 pub script_errors: Vec<String>,
523 pub script_output: Vec<Vec<String>>,
525 pub script_output_expanded: Vec<bool>,
527 pub script_panels: Vec<Option<(String, String)>>,
529
530 pub open_log_requested: bool,
532
533 pub identify_panes_requested: bool,
535
536 pub update_install_requested: bool,
539 pub check_now_requested: bool,
541 pub update_status: Option<String>,
543 pub update_result: Option<Result<UpdateResult, String>>,
545 pub last_update_result: Option<UpdateCheckResult>,
547 pub update_installing: bool,
549 update_install_receiver: Option<std::sync::mpsc::Receiver<Result<UpdateResult, String>>>,
551
552 pub editing_snippet_index: Option<usize>,
555 pub temp_snippet_id: String,
557 pub temp_snippet_title: String,
559 pub temp_snippet_content: String,
561 pub temp_snippet_keybinding: String,
563 pub temp_snippet_folder: String,
565 pub temp_snippet_description: String,
567 pub temp_snippet_keybinding_enabled: bool,
569 pub temp_snippet_auto_execute: bool,
571 pub temp_snippet_variables: Vec<(String, String)>,
573 pub adding_new_snippet: bool,
575 pub recording_snippet_keybinding: bool,
577 pub snippet_recorded_combo: Option<String>,
579
580 pub editing_action_index: Option<usize>,
583 pub temp_action_type: usize,
585 pub temp_action_id: String,
587 pub temp_action_title: String,
589 pub temp_action_command: String,
591 pub temp_action_args: String,
593 pub temp_action_text: String,
595 pub temp_action_keys: String,
597 pub temp_action_keybinding: String,
599 pub adding_new_action: bool,
601 pub recording_action_keybinding: bool,
603 pub action_recorded_combo: Option<String>,
605
606 pub dynamic_source_editing: Option<usize>,
609 pub dynamic_source_edit_buffer: Option<par_term_config::DynamicProfileSource>,
611 pub dynamic_source_new_header_key: String,
613 pub dynamic_source_new_header_value: String,
615
616 pub temp_import_url: String,
619 pub import_export_status: Option<String>,
621 pub import_export_is_error: bool,
623
624 pub show_reset_defaults_dialog: bool,
627
628 pub arrangement_save_name: String,
631 pub arrangement_confirm_restore: Option<ArrangementId>,
633 pub arrangement_confirm_delete: Option<ArrangementId>,
635 pub arrangement_confirm_overwrite: Option<String>,
637 pub arrangement_rename_id: Option<ArrangementId>,
639 pub arrangement_rename_text: String,
641 pub pending_arrangement_actions: Vec<SettingsWindowAction>,
643 pub arrangement_manager: ArrangementManager,
645
646 pub app_version: &'static str,
649
650 pub installation_type: InstallationType,
652
653 pub shader_install_fn: Option<fn(bool) -> Result<ShaderInstallResult, String>>,
655
656 pub shader_detect_modified_fn: Option<ShaderDetectModifiedFn>,
658
659 pub shader_uninstall_fn: Option<fn(bool) -> Result<ShaderUninstallResult, String>>,
661
662 pub shader_has_files_fn: Option<fn(&std::path::Path) -> bool>,
664
665 pub shader_count_files_fn: Option<fn(&std::path::Path) -> usize>,
667
668 pub shell_integration_is_installed_fn: Option<fn() -> bool>,
670
671 pub shell_integration_detected_shell_fn: Option<fn() -> par_term_config::ShellType>,
673
674 pub shell_integration_install_fn: Option<fn() -> Result<ShellIntegrationInstallResult, String>>,
676
677 pub shell_integration_uninstall_fn:
679 Option<fn() -> Result<ShellIntegrationUninstallResult, String>>,
680}
681
682impl SettingsUI {
683 pub fn new(config: Config) -> Self {
685 let initial_cols = config.cols;
687 let initial_rows = config.rows;
688 let initial_collapsed: HashSet<String> =
689 config.collapsed_settings_sections.iter().cloned().collect();
690
691 Self {
692 visible: false,
693 temp_font_bold: config.font_family_bold.clone().unwrap_or_default(),
694 temp_font_italic: config.font_family_italic.clone().unwrap_or_default(),
695 temp_font_bold_italic: config.font_family_bold_italic.clone().unwrap_or_default(),
696 temp_font_family: config.font_family.clone(),
697 temp_font_size: config.font_size,
698 temp_line_spacing: config.line_spacing,
699 temp_char_spacing: config.char_spacing,
700 temp_enable_text_shaping: config.enable_text_shaping,
701 temp_enable_ligatures: config.enable_ligatures,
702 temp_enable_kerning: config.enable_kerning,
703 font_pending_changes: false,
704 temp_custom_shell: config.custom_shell.clone().unwrap_or_default(),
705 temp_shell_args: config
706 .shell_args
707 .as_ref()
708 .map(|args| args.join(" "))
709 .unwrap_or_default(),
710 temp_working_directory: config.working_directory.clone().unwrap_or_default(),
711 temp_startup_directory: config.startup_directory.clone().unwrap_or_default(),
712 temp_initial_text: config.initial_text.clone(),
713 temp_background_image: config.background_image.clone().unwrap_or_default(),
714 temp_custom_shader: config.custom_shader.clone().unwrap_or_default(),
715 temp_cursor_shader: config.cursor_shader.clone().unwrap_or_default(),
716 temp_shader_channel0: config.custom_shader_channel0.clone().unwrap_or_default(),
717 temp_shader_channel1: config.custom_shader_channel1.clone().unwrap_or_default(),
718 temp_shader_channel2: config.custom_shader_channel2.clone().unwrap_or_default(),
719 temp_shader_channel3: config.custom_shader_channel3.clone().unwrap_or_default(),
720 temp_cubemap_path: config.custom_shader_cubemap.clone().unwrap_or_default(),
721 temp_background_color: config.background_color,
722 temp_pane_bg_path: String::new(),
723 temp_pane_bg_mode: BackgroundImageMode::default(),
724 temp_pane_bg_opacity: 1.0,
725 temp_pane_bg_darken: 0.0,
726 temp_pane_bg_index: None,
727 last_live_opacity: config.window_opacity,
728 current_cols: initial_cols,
729 current_rows: initial_rows,
730 supported_vsync_modes: vec![
731 par_term_config::VsyncMode::Immediate,
732 par_term_config::VsyncMode::Mailbox,
733 par_term_config::VsyncMode::Fifo,
734 ],
735 vsync_warning: None,
736 config,
737 has_changes: false,
738 search_query: String::new(),
739 focus_search: true,
740 shader_editor_visible: false,
741 shader_editor_source: String::new(),
742 shader_editor_error: None,
743 shader_editor_original: String::new(),
744 cursor_shader_editor_visible: false,
745 cursor_shader_editor_source: String::new(),
746 cursor_shader_editor_error: None,
747 cursor_shader_editor_original: String::new(),
748 available_agent_ids: Vec::new(),
749 available_shaders: Self::scan_shaders_folder(),
750 available_cubemaps: Self::scan_cubemaps_folder(),
751 new_shader_name: String::new(),
752 show_create_shader_dialog: false,
753 show_delete_shader_dialog: false,
754 shader_search_query: String::new(),
755 shader_search_matches: Vec::new(),
756 shader_search_current: 0,
757 shader_search_visible: false,
758 shader_metadata_cache: ShaderMetadataCache::with_shaders_dir(
759 par_term_config::Config::shaders_dir(),
760 ),
761 cursor_shader_metadata_cache: CursorShaderMetadataCache::with_shaders_dir(
762 par_term_config::Config::shaders_dir(),
763 ),
764 shader_settings_expanded: true,
765 cursor_shader_settings_expanded: true,
766 keybinding_recording_index: None,
767 keybinding_recorded_combo: None,
768 test_notification_requested: false,
769 selected_tab: SettingsTab::default(),
770 collapsed_sections: initial_collapsed,
771 shell_integration_action: None,
772 profile_modal_ui: ProfileModalUI::new(),
773 profile_save_requested: false,
774 profile_open_requested: None,
775 shader_installing: false,
776 shader_status: None,
777 shader_error: None,
778 shader_overwrite_prompt_visible: false,
779 shader_conflicts: Vec::new(),
780 shader_install_receiver: None,
781 editing_trigger_index: None,
782 temp_trigger_name: String::new(),
783 temp_trigger_pattern: String::new(),
784 temp_trigger_actions: Vec::new(),
785 adding_new_trigger: false,
786 trigger_pattern_error: None,
787 editing_coprocess_index: None,
788 temp_coprocess_name: String::new(),
789 temp_coprocess_command: String::new(),
790 temp_coprocess_args: String::new(),
791 temp_coprocess_auto_start: false,
792 temp_coprocess_copy_output: true,
793 temp_coprocess_restart_policy: par_term_config::automation::RestartPolicy::Never,
794 temp_coprocess_restart_delay_ms: 0,
795 adding_new_coprocess: false,
796 trigger_resync_requested: false,
797 pending_coprocess_actions: Vec::new(),
798 coprocess_running: Vec::new(),
799 coprocess_errors: Vec::new(),
800 coprocess_output: Vec::new(),
801 coprocess_output_expanded: Vec::new(),
802 editing_script_index: None,
803 temp_script_name: String::new(),
804 temp_script_path: String::new(),
805 temp_script_args: String::new(),
806 temp_script_auto_start: false,
807 temp_script_enabled: true,
808 temp_script_restart_policy: par_term_config::automation::RestartPolicy::Never,
809 temp_script_restart_delay_ms: 0,
810 temp_script_subscriptions: String::new(),
811 adding_new_script: false,
812 pending_script_actions: Vec::new(),
813 script_running: Vec::new(),
814 script_errors: Vec::new(),
815 script_output: Vec::new(),
816 script_output_expanded: Vec::new(),
817 script_panels: Vec::new(),
818 open_log_requested: false,
819 identify_panes_requested: false,
820 update_install_requested: false,
821 check_now_requested: false,
822 update_status: None,
823 update_result: None,
824 last_update_result: None,
825 update_installing: false,
826 update_install_receiver: None,
827 editing_snippet_index: None,
828 temp_snippet_id: String::new(),
829 temp_snippet_title: String::new(),
830 temp_snippet_content: String::new(),
831 temp_snippet_keybinding: String::new(),
832 temp_snippet_folder: String::new(),
833 temp_snippet_description: String::new(),
834 temp_snippet_keybinding_enabled: true,
835 temp_snippet_auto_execute: false,
836 temp_snippet_variables: Vec::new(),
837 adding_new_snippet: false,
838 editing_action_index: None,
839 temp_action_type: 0,
840 temp_action_id: String::new(),
841 temp_action_title: String::new(),
842 temp_action_command: String::new(),
843 temp_action_args: String::new(),
844 temp_action_text: String::new(),
845 temp_action_keys: String::new(),
846 temp_action_keybinding: String::new(),
847 adding_new_action: false,
848 recording_snippet_keybinding: false,
849 snippet_recorded_combo: None,
850 recording_action_keybinding: false,
851 action_recorded_combo: None,
852 dynamic_source_editing: None,
853 dynamic_source_edit_buffer: None,
854 dynamic_source_new_header_key: String::new(),
855 dynamic_source_new_header_value: String::new(),
856 temp_import_url: String::new(),
857 import_export_status: None,
858 import_export_is_error: false,
859 show_reset_defaults_dialog: false,
860 arrangement_save_name: String::new(),
861 arrangement_confirm_restore: None,
862 arrangement_confirm_delete: None,
863 arrangement_confirm_overwrite: None,
864 arrangement_rename_id: None,
865 arrangement_rename_text: String::new(),
866 pending_arrangement_actions: Vec::new(),
867 arrangement_manager: ArrangementManager::new(),
868 app_version: "",
869 installation_type: InstallationType::StandaloneBinary,
870 shader_install_fn: None,
871 shader_detect_modified_fn: None,
872 shader_uninstall_fn: None,
873 shader_has_files_fn: None,
874 shader_count_files_fn: None,
875 shell_integration_is_installed_fn: None,
876 shell_integration_detected_shell_fn: None,
877 shell_integration_install_fn: None,
878 shell_integration_uninstall_fn: None,
879 }
880 }
881
882 pub fn update_current_size(&mut self, cols: usize, rows: usize) {
884 self.current_cols = cols;
885 self.current_rows = rows;
886 }
887
888 pub fn update_supported_vsync_modes(&mut self, modes: Vec<par_term_config::VsyncMode>) {
890 self.supported_vsync_modes = modes;
891 self.vsync_warning = None;
892 }
893
894 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
896 self.supported_vsync_modes.contains(&mode)
897 }
898
899 #[allow(dead_code)]
901 pub fn set_vsync_warning(&mut self, warning: Option<String>) {
902 self.vsync_warning = warning;
903 }
904
905 pub fn pick_file_path(&self, title: &str) -> Option<String> {
906 FileDialog::new()
907 .set_title(title)
908 .pick_file()
909 .map(|p| p.display().to_string())
910 }
911
912 pub fn pick_folder_path(&self, title: &str) -> Option<String> {
913 FileDialog::new()
914 .set_title(title)
915 .pick_folder()
916 .map(|p| p.display().to_string())
917 }
918
919 pub fn update_config(&mut self, config: Config) {
921 if !self.has_changes {
922 self.config = config;
923 self.last_live_opacity = self.config.window_opacity;
924 if !self.font_pending_changes {
925 self.sync_font_temps_from_config();
926 }
927 }
928 }
929
930 pub fn force_update_config(&mut self, config: Config) {
932 self.config = config;
933 self.sync_all_temps_from_config();
934 self.has_changes = false;
935 }
936
937 fn sync_font_temps_from_config(&mut self) {
938 self.temp_font_family = self.config.font_family.clone();
939 self.temp_font_size = self.config.font_size;
940 self.temp_line_spacing = self.config.line_spacing;
941 self.temp_char_spacing = self.config.char_spacing;
942 self.temp_enable_text_shaping = self.config.enable_text_shaping;
943 self.temp_enable_ligatures = self.config.enable_ligatures;
944 self.temp_enable_kerning = self.config.enable_kerning;
945 self.temp_font_bold = self.config.font_family_bold.clone().unwrap_or_default();
946 self.temp_font_italic = self.config.font_family_italic.clone().unwrap_or_default();
947 self.temp_font_bold_italic = self
948 .config
949 .font_family_bold_italic
950 .clone()
951 .unwrap_or_default();
952 self.font_pending_changes = false;
953 }
954
955 pub fn sync_all_temps_from_config(&mut self) {
957 self.sync_font_temps_from_config();
958 self.temp_custom_shell = self.config.custom_shell.clone().unwrap_or_default();
959 self.temp_shell_args = self
960 .config
961 .shell_args
962 .as_ref()
963 .map(|args| args.join(" "))
964 .unwrap_or_default();
965 self.temp_working_directory = self.config.working_directory.clone().unwrap_or_default();
966 self.temp_startup_directory = self.config.startup_directory.clone().unwrap_or_default();
967 self.temp_initial_text = self.config.initial_text.clone();
968 self.temp_background_image = self.config.background_image.clone().unwrap_or_default();
969 self.temp_background_color = self.config.background_color;
970 self.temp_custom_shader = self.config.custom_shader.clone().unwrap_or_default();
971 self.temp_cursor_shader = self.config.cursor_shader.clone().unwrap_or_default();
972 self.temp_shader_channel0 = self
973 .config
974 .custom_shader_channel0
975 .clone()
976 .unwrap_or_default();
977 self.temp_shader_channel1 = self
978 .config
979 .custom_shader_channel1
980 .clone()
981 .unwrap_or_default();
982 self.temp_shader_channel2 = self
983 .config
984 .custom_shader_channel2
985 .clone()
986 .unwrap_or_default();
987 self.temp_shader_channel3 = self
988 .config
989 .custom_shader_channel3
990 .clone()
991 .unwrap_or_default();
992 self.temp_cubemap_path = self
993 .config
994 .custom_shader_cubemap
995 .clone()
996 .unwrap_or_default();
997 self.last_live_opacity = self.config.window_opacity;
998 }
999
1000 fn reset_all_to_defaults(&mut self) {
1002 self.config = Config::default();
1003 self.sync_all_temps_from_config();
1004 self.has_changes = true;
1005 self.search_query.clear();
1006 }
1007
1008 fn show_reset_defaults_dialog_window(&mut self, ctx: &Context) {
1010 if !self.show_reset_defaults_dialog {
1011 return;
1012 }
1013
1014 let mut close_dialog = false;
1015 let mut do_reset = false;
1016
1017 egui::Window::new("Reset to Defaults")
1018 .collapsible(false)
1019 .resizable(false)
1020 .order(egui::Order::Foreground)
1021 .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
1022 .show(ctx, |ui| {
1023 ui.vertical_centered(|ui| {
1024 ui.add_space(10.0);
1025 ui.label(
1026 egui::RichText::new("âš Warning")
1027 .color(egui::Color32::YELLOW)
1028 .size(18.0)
1029 .strong(),
1030 );
1031 ui.add_space(10.0);
1032 ui.label("This will reset ALL settings to their default values.");
1033 ui.add_space(5.0);
1034 ui.label(
1035 egui::RichText::new("Unsaved changes will be lost. This cannot be undone.")
1036 .color(egui::Color32::GRAY),
1037 );
1038 ui.add_space(15.0);
1039
1040 ui.horizontal(|ui| {
1041 let reset_button = egui::Button::new(
1042 egui::RichText::new("Reset").color(egui::Color32::WHITE),
1043 )
1044 .fill(egui::Color32::from_rgb(180, 50, 50));
1045
1046 if ui.add(reset_button).clicked() {
1047 do_reset = true;
1048 close_dialog = true;
1049 }
1050
1051 ui.add_space(10.0);
1052
1053 if ui.button("Cancel").clicked() {
1054 close_dialog = true;
1055 }
1056 });
1057 ui.add_space(10.0);
1058 });
1059 });
1060
1061 if do_reset {
1062 self.reset_all_to_defaults();
1063 }
1064
1065 if close_dialog {
1066 self.show_reset_defaults_dialog = false;
1067 }
1068 }
1069
1070 pub fn start_shader_install_with<F>(&mut self, force_overwrite: bool, install_fn: F)
1073 where
1074 F: FnOnce(bool) -> Result<ShaderInstallResult, String> + Send + 'static,
1075 {
1076 use std::sync::mpsc;
1077
1078 if self.shader_installing {
1079 return;
1080 }
1081
1082 self.shader_error = None;
1083 self.shader_status = Some(if force_overwrite {
1084 "Reinstalling shaders (overwriting modified files)...".to_string()
1085 } else {
1086 "Reinstalling shaders...".to_string()
1087 });
1088 self.shader_installing = true;
1089
1090 let (tx, rx) = mpsc::channel();
1091 self.shader_install_receiver = Some(rx);
1092
1093 std::thread::spawn(move || {
1094 let result = install_fn(force_overwrite);
1095 let _ = tx.send(result);
1096 });
1097 }
1098
1099 pub fn poll_shader_install_status(&mut self) {
1101 if let Some(receiver) = &self.shader_install_receiver
1102 && let Ok(result) = receiver.try_recv()
1103 {
1104 self.shader_installing = false;
1105 self.shader_install_receiver = None;
1106 match result {
1107 Ok(res) => {
1108 let detail = if res.skipped > 0 {
1109 format!(
1110 "Installed {} shaders ({} skipped, {} removed)",
1111 res.installed, res.skipped, res.removed
1112 )
1113 } else {
1114 format!(
1115 "Installed {} shaders ({} removed)",
1116 res.installed, res.removed
1117 )
1118 };
1119 self.shader_status = Some(detail);
1120 self.shader_error = None;
1121 self.config.integration_versions.shaders_installed_version =
1122 Some(self.app_version.to_string());
1123 }
1124 Err(e) => {
1125 self.shader_error = Some(e);
1126 self.shader_status = None;
1127 }
1128 }
1129 }
1130 }
1131
1132 pub fn start_self_update_with<F>(&mut self, version: String, update_fn: F)
1135 where
1136 F: FnOnce(&str) -> Result<UpdateResult, String> + Send + 'static,
1137 {
1138 use std::sync::mpsc;
1139
1140 if self.update_installing {
1141 return;
1142 }
1143
1144 self.update_status = Some("Downloading and installing update...".to_string());
1145 self.update_result = None;
1146 self.update_installing = true;
1147
1148 let (tx, rx) = mpsc::channel();
1149 self.update_install_receiver = Some(rx);
1150
1151 std::thread::spawn(move || {
1152 let result = update_fn(&version);
1153 let _ = tx.send(result);
1154 });
1155 }
1156
1157 pub fn poll_update_install_status(&mut self) {
1159 if let Some(receiver) = &self.update_install_receiver
1160 && let Ok(result) = receiver.try_recv()
1161 {
1162 self.update_installing = false;
1163 self.update_install_receiver = None;
1164 match &result {
1165 Ok(res) => {
1166 self.update_status = Some(format!(
1167 "Update installed! Restart par-term to use v{}",
1168 res.new_version
1169 ));
1170 }
1171 Err(e) => {
1172 self.update_status = Some(format!("Update failed: {}", e));
1173 }
1174 }
1175 self.update_result = Some(result);
1176 }
1177 }
1178
1179 pub fn apply_font_changes(&mut self) {
1181 self.config.font_family = self.temp_font_family.clone();
1182 self.config.font_size = self.temp_font_size;
1183 self.config.line_spacing = self.temp_line_spacing;
1184 self.config.char_spacing = self.temp_char_spacing;
1185 self.config.enable_text_shaping = self.temp_enable_text_shaping;
1186 self.config.enable_ligatures = self.temp_enable_ligatures;
1187 self.config.enable_kerning = self.temp_enable_kerning;
1188 self.config.font_family_bold = if self.temp_font_bold.is_empty() {
1189 None
1190 } else {
1191 Some(self.temp_font_bold.clone())
1192 };
1193 self.config.font_family_italic = if self.temp_font_italic.is_empty() {
1194 None
1195 } else {
1196 Some(self.temp_font_italic.clone())
1197 };
1198 self.config.font_family_bold_italic = if self.temp_font_bold_italic.is_empty() {
1199 None
1200 } else {
1201 Some(self.temp_font_bold_italic.clone())
1202 };
1203 self.font_pending_changes = false;
1204 }
1205
1206 #[allow(dead_code)]
1208 pub fn toggle(&mut self) {
1209 self.visible = !self.visible;
1210 if self.visible {
1211 self.focus_search = true;
1212 }
1213 }
1214
1215 #[allow(dead_code)]
1217 pub fn current_config(&self) -> &Config {
1218 &self.config
1219 }
1220
1221 fn sync_collapsed_sections_to_config(&mut self) {
1223 self.config.collapsed_settings_sections = self.collapsed_sections.iter().cloned().collect();
1224 }
1225
1226 pub fn collapsed_sections_snapshot(&self) -> Vec<String> {
1228 self.collapsed_sections.iter().cloned().collect()
1229 }
1230
1231 pub fn take_test_notification_request(&mut self) -> bool {
1233 let requested = self.test_notification_requested;
1234 self.test_notification_requested = false;
1235 requested
1236 }
1237
1238 pub fn sync_profiles(&mut self, profiles: Vec<Profile>) {
1240 self.profile_modal_ui.load_profiles(profiles);
1241 }
1242
1243 pub fn take_profile_save_request(&mut self) -> Option<Vec<Profile>> {
1245 if self.profile_save_requested {
1246 self.profile_save_requested = false;
1247 Some(self.profile_modal_ui.get_working_profiles().to_vec())
1248 } else {
1249 None
1250 }
1251 }
1252
1253 pub fn take_profile_open_request(&mut self) -> Option<ProfileId> {
1255 self.profile_open_requested.take()
1256 }
1257
1258 #[allow(dead_code)]
1260 pub fn show(
1261 &mut self,
1262 ctx: &Context,
1263 ) -> (
1264 Option<Config>,
1265 Option<Config>,
1266 Option<ShaderEditorResult>,
1267 Option<CursorShaderEditorResult>,
1268 ) {
1269 if !self.visible && !self.shader_editor_visible && !self.cursor_shader_editor_visible {
1270 return (None, None, None, None);
1271 }
1272
1273 log::info!("SettingsUI.show() called - visible: true");
1274
1275 if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
1276 if self.cursor_shader_editor_visible {
1277 self.cursor_shader_editor_visible = false;
1278 self.cursor_shader_editor_error = None;
1279 } else if self.shader_editor_visible {
1280 self.shader_editor_visible = false;
1281 self.shader_editor_error = None;
1282 } else if self.visible {
1283 self.visible = false;
1284 return (None, None, None, None);
1285 }
1286 }
1287
1288 let mut style = (*ctx.style()).clone();
1289 let solid_bg = Color32::from_rgba_unmultiplied(24, 24, 24, 255);
1290 style.visuals.window_fill = solid_bg;
1291 style.visuals.panel_fill = solid_bg;
1292 style.visuals.widgets.noninteractive.bg_fill = solid_bg;
1293 ctx.set_style(style);
1294
1295 let mut save_requested = false;
1296 let mut discard_requested = false;
1297 let mut close_requested = false;
1298 let mut open = true;
1299 let mut changes_this_frame = false;
1300
1301 if self.visible {
1302 let settings_viewport = ctx.input(|i| i.viewport_rect());
1303 Window::new("Settings")
1304 .resizable(true)
1305 .default_width(650.0)
1306 .default_height(700.0)
1307 .default_pos(settings_viewport.center())
1308 .pivot(egui::Align2::CENTER_CENTER)
1309 .open(&mut open)
1310 .frame(
1311 Frame::window(&ctx.style())
1312 .fill(solid_bg)
1313 .stroke(egui::Stroke::NONE)
1314 .shadow(Shadow {
1315 offset: [0, 0],
1316 blur: 0,
1317 spread: 0,
1318 color: Color32::TRANSPARENT,
1319 }),
1320 )
1321 .show(ctx, |ui| {
1322 ui.heading("Terminal Settings");
1324 ui.horizontal(|ui| {
1325 ui.label("Quick search:");
1326 let response = ui.add(
1327 egui::TextEdit::singleline(&mut self.search_query)
1328 .hint_text("Type to filter settings"),
1329 );
1330 if self.focus_search {
1331 self.focus_search = false;
1332 response.request_focus();
1333 }
1334 });
1335 ui.separator();
1336
1337 self.show_settings_sections(ui, &mut changes_this_frame);
1340
1341 ui.separator();
1343 ui.horizontal(|ui| {
1344 if ui.button("Save").clicked() {
1345 save_requested = true;
1346 }
1347 if ui.button("Discard").clicked() {
1348 discard_requested = true;
1349 }
1350 if ui.button("Close").clicked() {
1351 close_requested = true;
1352 }
1353 ui.separator();
1354 if ui
1355 .button("Edit Config File")
1356 .on_hover_text("Open config.yaml in your default editor")
1357 .clicked()
1358 {
1359 let config_path = Config::config_path();
1360 if let Err(e) = open::that(&config_path) {
1361 log::error!("Failed to open config file: {}", e);
1362 }
1363 }
1364 if ui
1365 .button("Reset to Defaults")
1366 .on_hover_text("Reset all settings to their default values")
1367 .clicked()
1368 {
1369 self.show_reset_defaults_dialog = true;
1370 }
1371 if self.has_changes {
1372 ui.colored_label(egui::Color32::YELLOW, "* Unsaved changes");
1373 }
1374 });
1375 });
1376 }
1377
1378 let shader_apply_result = self.show_shader_editor_window(ctx);
1379 let cursor_shader_apply_result = self.show_cursor_shader_editor_window(ctx);
1380
1381 self.show_create_shader_dialog_window(ctx);
1382 self.show_delete_shader_dialog_window(ctx);
1383 self.show_reset_defaults_dialog_window(ctx);
1384
1385 if self.visible && (!open || close_requested) {
1386 self.visible = false;
1387 }
1388
1389 let config_to_save = if save_requested {
1390 if self.font_pending_changes {
1391 self.apply_font_changes();
1392 }
1393 self.has_changes = false;
1394 self.sync_collapsed_sections_to_config();
1395 let mut config = self.config.clone();
1396 config.generate_snippet_action_keybindings();
1397 Some(config)
1398 } else {
1399 None
1400 };
1401
1402 if discard_requested {
1403 self.has_changes = false;
1404 self.sync_font_temps_from_config();
1405 }
1406
1407 let config_for_live_update = if self.visible {
1408 if (self.config.window_opacity - self.last_live_opacity).abs() > f32::EPSILON {
1409 log::info!(
1410 "SettingsUI: live opacity {:.3} (last {:.3})",
1411 self.config.window_opacity,
1412 self.last_live_opacity
1413 );
1414 self.last_live_opacity = self.config.window_opacity;
1415 }
1416 Some(self.config.clone())
1417 } else {
1418 None
1419 };
1420
1421 (
1422 config_to_save,
1423 config_for_live_update,
1424 shader_apply_result,
1425 cursor_shader_apply_result,
1426 )
1427 }
1428
1429 pub fn show_as_panel(
1431 &mut self,
1432 ctx: &Context,
1433 ) -> (
1434 Option<Config>,
1435 Option<Config>,
1436 Option<ShaderEditorResult>,
1437 Option<CursorShaderEditorResult>,
1438 ) {
1439 if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
1440 if self.cursor_shader_editor_visible {
1441 self.cursor_shader_editor_visible = false;
1442 self.cursor_shader_editor_error = None;
1443 } else if self.shader_editor_visible {
1444 self.shader_editor_visible = false;
1445 self.shader_editor_error = None;
1446 }
1447 }
1448
1449 let mut style = (*ctx.style()).clone();
1450 let solid_bg = Color32::from_rgba_unmultiplied(24, 24, 24, 255);
1451 style.visuals.window_fill = solid_bg;
1452 style.visuals.panel_fill = solid_bg;
1453 style.visuals.widgets.noninteractive.bg_fill = solid_bg;
1454 ctx.set_style(style);
1455
1456 let mut save_requested = false;
1457 let mut discard_requested = false;
1458 let mut changes_this_frame = false;
1459
1460 egui::CentralPanel::default()
1461 .frame(Frame::central_panel(&ctx.style()).fill(solid_bg))
1462 .show(ctx, |ui| {
1463 ui.heading("Terminal Settings");
1465 ui.horizontal(|ui| {
1466 ui.label("Quick search:");
1467 let response = ui.add(
1468 egui::TextEdit::singleline(&mut self.search_query)
1469 .hint_text("Type to filter settings"),
1470 );
1471 if self.focus_search {
1472 self.focus_search = false;
1473 response.request_focus();
1474 }
1475 });
1476 ui.separator();
1477
1478 self.show_settings_sections(ui, &mut changes_this_frame);
1481
1482 ui.separator();
1484 ui.horizontal(|ui| {
1485 if ui.button("Save").clicked() {
1486 save_requested = true;
1487 }
1488 if ui.button("Discard").clicked() {
1489 discard_requested = true;
1490 }
1491 ui.separator();
1492 if ui
1493 .button("Edit Config File")
1494 .on_hover_text("Open config.yaml in your default editor")
1495 .clicked()
1496 {
1497 let config_path = Config::config_path();
1498 if let Err(e) = open::that(&config_path) {
1499 log::error!("Failed to open config file: {}", e);
1500 }
1501 }
1502 if ui
1503 .button("Reset to Defaults")
1504 .on_hover_text("Reset all settings to their default values")
1505 .clicked()
1506 {
1507 self.show_reset_defaults_dialog = true;
1508 }
1509 if self.has_changes {
1510 ui.colored_label(egui::Color32::YELLOW, "* Unsaved changes");
1511 }
1512 });
1513 });
1514
1515 let shader_apply_result = self.show_shader_editor_window(ctx);
1516 let cursor_shader_apply_result = self.show_cursor_shader_editor_window(ctx);
1517
1518 self.show_create_shader_dialog_window(ctx);
1519 self.show_delete_shader_dialog_window(ctx);
1520 self.show_reset_defaults_dialog_window(ctx);
1521
1522 let config_to_save = if save_requested {
1523 if self.font_pending_changes {
1524 self.apply_font_changes();
1525 }
1526 self.has_changes = false;
1527 self.sync_collapsed_sections_to_config();
1528 let mut config = self.config.clone();
1529 config.generate_snippet_action_keybindings();
1530 Some(config)
1531 } else {
1532 None
1533 };
1534
1535 if discard_requested {
1536 self.has_changes = false;
1537 self.sync_font_temps_from_config();
1538 }
1539
1540 let config_for_live_update = {
1541 if (self.config.window_opacity - self.last_live_opacity).abs() > f32::EPSILON {
1542 log::info!(
1543 "SettingsUI: live opacity {:.3} (last {:.3})",
1544 self.config.window_opacity,
1545 self.last_live_opacity
1546 );
1547 self.last_live_opacity = self.config.window_opacity;
1548 }
1549 Some(self.config.clone())
1550 };
1551
1552 (
1553 config_to_save,
1554 config_for_live_update,
1555 shader_apply_result,
1556 cursor_shader_apply_result,
1557 )
1558 }
1559
1560 fn show_settings_sections(&mut self, ui: &mut egui::Ui, changes_this_frame: &mut bool) {
1562 quick_settings::show(ui, self, changes_this_frame);
1563 ui.separator();
1564
1565 let available_width = ui.available_width();
1566 let footer_height = 45.0;
1568 let available_height = (ui.available_height() - footer_height).max(100.0);
1569 let sidebar_width = 150.0;
1570 let content_width = (available_width - sidebar_width - 15.0).max(300.0);
1571
1572 let layout = egui::Layout::left_to_right(egui::Align::Min);
1573 ui.allocate_ui_with_layout(
1574 egui::vec2(available_width, available_height),
1575 layout,
1576 |ui| {
1577 ui.allocate_ui_with_layout(
1579 egui::vec2(sidebar_width, available_height),
1580 egui::Layout::top_down(egui::Align::Min),
1581 |ui| {
1582 egui::ScrollArea::vertical()
1583 .id_salt("settings_sidebar")
1584 .max_height(available_height)
1585 .show(ui, |ui| {
1586 sidebar::show(ui, &mut self.selected_tab, &self.search_query);
1587 });
1588 },
1589 );
1590
1591 ui.separator();
1592
1593 ui.allocate_ui_with_layout(
1595 egui::vec2(content_width, available_height),
1596 egui::Layout::top_down(egui::Align::Min),
1597 |ui| {
1598 egui::ScrollArea::vertical()
1599 .id_salt("settings_tab_content")
1600 .max_height(available_height)
1601 .show(ui, |ui| {
1602 ui.set_min_width(content_width - 20.0);
1603 self.show_tab_content(ui, changes_this_frame);
1604 });
1605 },
1606 );
1607 },
1608 );
1609 }
1610
1611 fn show_tab_content(&mut self, ui: &mut egui::Ui, changes_this_frame: &mut bool) {
1613 let mut collapsed = std::mem::take(&mut self.collapsed_sections);
1614
1615 match self.selected_tab {
1616 SettingsTab::Appearance => {
1617 appearance_tab::show(ui, self, changes_this_frame, &mut collapsed);
1618 }
1619 SettingsTab::Window => {
1620 window_tab::show(ui, self, changes_this_frame, &mut collapsed);
1621 }
1622 SettingsTab::Input => {
1623 input_tab::show(ui, self, changes_this_frame, &mut collapsed);
1624 }
1625 SettingsTab::Terminal => {
1626 terminal_tab::show(ui, self, changes_this_frame, &mut collapsed);
1627 }
1628 SettingsTab::Effects => {
1629 effects_tab::show(ui, self, changes_this_frame, &mut collapsed);
1630 }
1631 SettingsTab::Badge => {
1632 badge_tab::show(ui, self, changes_this_frame, &mut collapsed);
1633 }
1634 SettingsTab::ProgressBar => {
1635 progress_bar_tab::show(ui, self, changes_this_frame, &mut collapsed);
1636 }
1637 SettingsTab::StatusBar => {
1638 status_bar_tab::show(ui, self, changes_this_frame, &mut collapsed);
1639 }
1640 SettingsTab::Profiles => {
1641 profiles_tab::show(ui, self, changes_this_frame, &mut collapsed);
1642 }
1643 SettingsTab::Ssh => {
1644 self.show_ssh_tab(ui, changes_this_frame);
1645 }
1646 SettingsTab::Notifications => {
1647 notifications_tab::show(ui, self, changes_this_frame, &mut collapsed);
1648 }
1649 SettingsTab::Integrations => {
1650 self.show_integrations_tab(ui, changes_this_frame, &mut collapsed);
1651 }
1652 SettingsTab::Automation => {
1653 automation_tab::show(ui, self, changes_this_frame, &mut collapsed);
1654 }
1655 SettingsTab::Scripts => {
1656 scripts_tab::show(ui, self, changes_this_frame, &mut collapsed);
1657 }
1658 SettingsTab::Snippets => {
1659 snippets_tab::show(ui, self, changes_this_frame, &mut collapsed);
1660 }
1661 SettingsTab::Actions => {
1662 actions_tab::show(ui, self, changes_this_frame, &mut collapsed);
1663 }
1664 SettingsTab::Arrangements => {
1665 arrangements_tab::show(ui, self, changes_this_frame, &mut collapsed);
1666 }
1667 SettingsTab::AiInspector => {
1668 ai_inspector_tab::show(ui, self, changes_this_frame, &mut collapsed);
1669 }
1670 SettingsTab::Advanced => {
1671 advanced_tab::show(ui, self, changes_this_frame, &mut collapsed);
1672 }
1673 }
1674
1675 self.collapsed_sections = collapsed;
1676 }
1677
1678 pub fn check_keybinding_conflict(&self, key: &str, exclude_id: Option<&str>) -> Option<String> {
1680 for binding in &self.config.keybindings {
1681 if binding.key == key {
1682 return Some(format!("Already bound to: {}", binding.action));
1683 }
1684 }
1685
1686 for snippet in &self.config.snippets {
1687 if let Some(snippet_key) = &snippet.keybinding
1688 && snippet_key == key
1689 {
1690 if exclude_id == Some(&snippet.id) {
1691 continue;
1692 }
1693 return Some(format!("Already bound to snippet: {}", snippet.title));
1694 }
1695 }
1696
1697 None
1698 }
1699}