1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
8#[serde(rename_all = "lowercase")]
9pub enum VsyncMode {
10 #[default]
12 Immediate,
13 Mailbox,
15 Fifo,
17}
18
19impl VsyncMode {
20 pub fn to_present_mode(self) -> wgpu::PresentMode {
22 match self {
23 VsyncMode::Immediate => wgpu::PresentMode::Immediate,
24 VsyncMode::Mailbox => wgpu::PresentMode::Mailbox,
25 VsyncMode::Fifo => wgpu::PresentMode::Fifo,
26 }
27 }
28}
29
30use crate::themes::Theme;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
34#[serde(rename_all = "lowercase")]
35pub enum CursorStyle {
36 #[default]
38 Block,
39 Beam,
41 Underline,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
47#[serde(rename_all = "lowercase")]
48pub enum BackgroundImageMode {
49 Fit,
51 Fill,
53 #[default]
55 Stretch,
56 Tile,
58 Center,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
64#[serde(rename_all = "snake_case")]
65pub enum TabBarMode {
66 Always,
68 #[default]
70 WhenMultiple,
71 Never,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct FontRange {
78 pub start: u32,
80 pub end: u32,
82 pub font_family: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Config {
90 #[serde(default = "default_cols")]
95 pub cols: usize,
96
97 #[serde(default = "default_rows")]
99 pub rows: usize,
100
101 #[serde(default = "default_font_size")]
103 pub font_size: f32,
104
105 #[serde(default = "default_font_family")]
107 pub font_family: String,
108
109 #[serde(default)]
111 pub font_family_bold: Option<String>,
112
113 #[serde(default)]
115 pub font_family_italic: Option<String>,
116
117 #[serde(default)]
119 pub font_family_bold_italic: Option<String>,
120
121 #[serde(default)]
125 pub font_ranges: Vec<FontRange>,
126
127 #[serde(default = "default_line_spacing")]
129 pub line_spacing: f32,
130
131 #[serde(default = "default_char_spacing")]
133 pub char_spacing: f32,
134
135 #[serde(default = "default_text_shaping")]
138 pub enable_text_shaping: bool,
139
140 #[serde(default = "default_true")]
142 pub enable_ligatures: bool,
143
144 #[serde(default = "default_true")]
146 pub enable_kerning: bool,
147
148 #[serde(default = "default_window_title")]
150 pub window_title: String,
151
152 #[serde(default = "default_true")]
155 pub allow_title_change: bool,
156
157 #[serde(default = "default_max_fps", alias = "refresh_rate")]
163 pub max_fps: u32,
164
165 #[serde(default)]
172 pub vsync_mode: VsyncMode,
173
174 #[serde(default = "default_window_padding")]
176 pub window_padding: f32,
177
178 #[serde(default = "default_window_opacity")]
180 pub window_opacity: f32,
181
182 #[serde(default = "default_false")]
184 pub window_always_on_top: bool,
185
186 #[serde(default = "default_true")]
188 pub window_decorations: bool,
189
190 #[serde(default = "default_window_width")]
192 pub window_width: u32,
193
194 #[serde(default = "default_window_height")]
196 pub window_height: u32,
197
198 #[serde(default)]
200 pub background_image: Option<String>,
201
202 #[serde(default = "default_true")]
204 pub background_image_enabled: bool,
205
206 #[serde(default)]
213 pub background_image_mode: BackgroundImageMode,
214
215 #[serde(default = "default_background_image_opacity")]
217 pub background_image_opacity: f32,
218
219 #[serde(default)]
223 pub custom_shader: Option<String>,
224
225 #[serde(default = "default_true")]
227 pub custom_shader_enabled: bool,
228
229 #[serde(default = "default_true")]
232 pub custom_shader_animation: bool,
233
234 #[serde(default = "default_custom_shader_speed")]
236 pub custom_shader_animation_speed: f32,
237
238 #[serde(default = "default_text_opacity")]
241 pub custom_shader_text_opacity: f32,
242
243 #[serde(default = "default_false")]
247 pub custom_shader_full_content: bool,
248
249 #[serde(default = "default_custom_shader_brightness")]
252 pub custom_shader_brightness: f32,
253
254 #[serde(default)]
257 pub custom_shader_channel1: Option<String>,
258
259 #[serde(default)]
261 pub custom_shader_channel2: Option<String>,
262
263 #[serde(default)]
265 pub custom_shader_channel3: Option<String>,
266
267 #[serde(default)]
269 pub custom_shader_channel4: Option<String>,
270
271 #[serde(default)]
277 pub cursor_shader: Option<String>,
278
279 #[serde(default = "default_false")]
281 pub cursor_shader_enabled: bool,
282
283 #[serde(default = "default_true")]
285 pub cursor_shader_animation: bool,
286
287 #[serde(default = "default_custom_shader_speed")]
289 pub cursor_shader_animation_speed: f32,
290
291 #[serde(default = "default_cursor_shader_color")]
294 pub cursor_shader_color: [u8; 3],
295
296 #[serde(default = "default_cursor_trail_duration")]
299 pub cursor_shader_trail_duration: f32,
300
301 #[serde(default = "default_cursor_glow_radius")]
304 pub cursor_shader_glow_radius: f32,
305
306 #[serde(default = "default_cursor_glow_intensity")]
309 pub cursor_shader_glow_intensity: f32,
310
311 #[serde(default = "default_false")]
315 pub cursor_shader_hides_cursor: bool,
316
317 #[serde(default = "default_true")]
322 pub auto_copy_selection: bool,
323
324 #[serde(default = "default_false", alias = "strip_trailing_newline_on_copy")]
327 pub copy_trailing_newline: bool,
328
329 #[serde(default = "default_true")]
331 pub middle_click_paste: bool,
332
333 #[serde(default = "default_scroll_speed")]
338 pub mouse_scroll_speed: f32,
339
340 #[serde(default = "default_double_click_threshold")]
342 pub mouse_double_click_threshold: u64,
343
344 #[serde(default = "default_triple_click_threshold")]
346 pub mouse_triple_click_threshold: u64,
347
348 #[serde(default = "default_scrollback", alias = "scrollback_size")]
353 pub scrollback_lines: usize,
354
355 #[serde(default = "default_false")]
357 pub cursor_blink: bool,
358
359 #[serde(default = "default_cursor_blink_interval")]
361 pub cursor_blink_interval: u64,
362
363 #[serde(default)]
365 pub cursor_style: CursorStyle,
366
367 #[serde(default = "default_cursor_color")]
369 pub cursor_color: [u8; 3],
370
371 #[serde(default = "default_scrollbar_autohide_delay")]
376 pub scrollbar_autohide_delay: u64,
377
378 #[serde(default = "default_theme")]
383 pub theme: String,
384
385 #[serde(default = "default_screenshot_format")]
390 pub screenshot_format: String,
391
392 #[serde(default = "default_true", alias = "close_on_shell_exit")]
397 pub exit_on_shell_exit: bool,
398
399 #[serde(default)]
401 pub custom_shell: Option<String>,
402
403 #[serde(default)]
405 pub shell_args: Option<Vec<String>>,
406
407 #[serde(default)]
409 pub working_directory: Option<String>,
410
411 #[serde(default)]
413 pub shell_env: Option<std::collections::HashMap<String, String>>,
414
415 #[serde(default = "default_login_shell")]
419 pub login_shell: bool,
420
421 #[serde(default = "default_scrollbar_position")]
426 pub scrollbar_position: String,
427
428 #[serde(default = "default_scrollbar_width")]
430 pub scrollbar_width: f32,
431
432 #[serde(default = "default_scrollbar_thumb_color")]
434 pub scrollbar_thumb_color: [f32; 4],
435
436 #[serde(default = "default_scrollbar_track_color")]
438 pub scrollbar_track_color: [f32; 4],
439
440 #[serde(
445 default = "default_clipboard_max_sync_events",
446 alias = "max_clipboard_sync_events"
447 )]
448 pub clipboard_max_sync_events: usize,
449
450 #[serde(
452 default = "default_clipboard_max_event_bytes",
453 alias = "max_clipboard_event_bytes"
454 )]
455 pub clipboard_max_event_bytes: usize,
456
457 #[serde(default = "default_false", alias = "bell_desktop")]
462 pub notification_bell_desktop: bool,
463
464 #[serde(default = "default_bell_sound", alias = "bell_sound")]
466 pub notification_bell_sound: u8,
467
468 #[serde(default = "default_true", alias = "bell_visual")]
470 pub notification_bell_visual: bool,
471
472 #[serde(default = "default_false", alias = "activity_notifications")]
474 pub notification_activity_enabled: bool,
475
476 #[serde(default = "default_activity_threshold", alias = "activity_threshold")]
478 pub notification_activity_threshold: u64,
479
480 #[serde(default = "default_false", alias = "silence_notifications")]
482 pub notification_silence_enabled: bool,
483
484 #[serde(default = "default_silence_threshold", alias = "silence_threshold")]
486 pub notification_silence_threshold: u64,
487
488 #[serde(
490 default = "default_notification_max_buffer",
491 alias = "max_notifications"
492 )]
493 pub notification_max_buffer: usize,
494
495 #[serde(default)]
500 pub tab_bar_mode: TabBarMode,
501
502 #[serde(default = "default_tab_bar_height")]
504 pub tab_bar_height: f32,
505
506 #[serde(default = "default_true")]
508 pub tab_show_close_button: bool,
509
510 #[serde(default = "default_false")]
512 pub tab_show_index: bool,
513
514 #[serde(default = "default_true")]
516 pub tab_inherit_cwd: bool,
517
518 #[serde(default = "default_zero")]
520 pub max_tabs: usize,
521}
522
523fn default_cols() -> usize {
525 80
526}
527
528fn default_rows() -> usize {
529 24
530}
531
532fn default_font_size() -> f32 {
533 13.0
534}
535
536fn default_font_family() -> String {
537 "JetBrains Mono".to_string()
538}
539
540fn default_line_spacing() -> f32 {
541 1.0 }
543
544fn default_char_spacing() -> f32 {
545 1.0 }
547
548fn default_text_shaping() -> bool {
549 true }
551
552fn default_scrollback() -> usize {
553 10000
554}
555
556fn default_window_title() -> String {
557 "par-term".to_string()
558}
559
560fn default_theme() -> String {
561 "dark-background".to_string()
562}
563
564fn default_screenshot_format() -> String {
565 "png".to_string()
566}
567
568fn default_max_fps() -> u32 {
569 60
570}
571
572fn default_window_padding() -> f32 {
573 10.0
574}
575
576fn default_login_shell() -> bool {
577 true
578}
579
580fn default_scrollbar_position() -> String {
581 "right".to_string()
582}
583
584fn default_scrollbar_width() -> f32 {
585 15.0
586}
587
588fn default_scrollbar_thumb_color() -> [f32; 4] {
589 [0.4, 0.4, 0.4, 0.95] }
591
592fn default_scrollbar_track_color() -> [f32; 4] {
593 [0.15, 0.15, 0.15, 0.6] }
595
596fn default_clipboard_max_sync_events() -> usize {
597 64 }
599
600fn default_clipboard_max_event_bytes() -> usize {
601 2048 }
603
604fn default_activity_threshold() -> u64 {
605 10 }
607
608fn default_silence_threshold() -> u64 {
609 300 }
611
612fn default_notification_max_buffer() -> usize {
613 64 }
615
616fn default_scroll_speed() -> f32 {
617 3.0 }
619
620fn default_double_click_threshold() -> u64 {
621 500 }
623
624fn default_triple_click_threshold() -> u64 {
625 500 }
627
628fn default_cursor_blink_interval() -> u64 {
629 500 }
631
632fn default_cursor_color() -> [u8; 3] {
633 [255, 255, 255] }
635
636fn default_scrollbar_autohide_delay() -> u64 {
637 0 }
639
640fn default_window_opacity() -> f32 {
641 1.0 }
643
644fn default_window_width() -> u32 {
645 1600 }
647
648fn default_window_height() -> u32 {
649 600 }
651
652fn default_background_image_opacity() -> f32 {
653 1.0 }
655
656fn default_false() -> bool {
657 false
658}
659
660fn default_true() -> bool {
661 true
662}
663
664fn default_text_opacity() -> f32 {
665 1.0 }
667
668fn default_custom_shader_speed() -> f32 {
669 1.0 }
671
672fn default_custom_shader_brightness() -> f32 {
673 1.0 }
675
676fn default_cursor_shader_color() -> [u8; 3] {
677 [255, 255, 255] }
679
680fn default_cursor_trail_duration() -> f32 {
681 0.5 }
683
684fn default_cursor_glow_radius() -> f32 {
685 80.0 }
687
688fn default_cursor_glow_intensity() -> f32 {
689 0.3 }
691
692fn default_bell_sound() -> u8 {
693 50 }
695
696fn default_tab_bar_height() -> f32 {
697 28.0 }
699
700fn default_zero() -> usize {
701 0
702}
703
704impl Default for Config {
705 fn default() -> Self {
706 Self {
707 cols: default_cols(),
708 rows: default_rows(),
709 font_size: default_font_size(),
710 font_family: default_font_family(),
711 font_family_bold: None,
712 font_family_italic: None,
713 font_family_bold_italic: None,
714 font_ranges: Vec::new(),
715 line_spacing: default_line_spacing(),
716 char_spacing: default_char_spacing(),
717 enable_text_shaping: default_text_shaping(),
718 enable_ligatures: default_true(),
719 enable_kerning: default_true(),
720 scrollback_lines: default_scrollback(),
721 cursor_blink: default_false(),
722 cursor_blink_interval: default_cursor_blink_interval(),
723 cursor_style: CursorStyle::default(),
724 cursor_color: default_cursor_color(),
725 scrollbar_autohide_delay: default_scrollbar_autohide_delay(),
726 window_title: default_window_title(),
727 allow_title_change: default_true(),
728 theme: default_theme(),
729 auto_copy_selection: default_true(),
730 copy_trailing_newline: default_false(),
731 middle_click_paste: default_true(),
732 mouse_scroll_speed: default_scroll_speed(),
733 mouse_double_click_threshold: default_double_click_threshold(),
734 mouse_triple_click_threshold: default_triple_click_threshold(),
735 screenshot_format: default_screenshot_format(),
736 max_fps: default_max_fps(),
737 vsync_mode: VsyncMode::default(),
738 window_padding: default_window_padding(),
739 window_opacity: default_window_opacity(),
740 window_always_on_top: default_false(),
741 window_decorations: default_true(),
742 window_width: default_window_width(),
743 window_height: default_window_height(),
744 background_image: None,
745 background_image_enabled: default_true(),
746 background_image_mode: BackgroundImageMode::default(),
747 background_image_opacity: default_background_image_opacity(),
748 custom_shader: None,
749 custom_shader_enabled: default_true(),
750 custom_shader_animation: default_true(),
751 custom_shader_animation_speed: default_custom_shader_speed(),
752 custom_shader_text_opacity: default_text_opacity(),
753 custom_shader_full_content: default_false(),
754 custom_shader_brightness: default_custom_shader_brightness(),
755 custom_shader_channel1: None,
756 custom_shader_channel2: None,
757 custom_shader_channel3: None,
758 custom_shader_channel4: None,
759 cursor_shader: None,
760 cursor_shader_enabled: default_false(),
761 cursor_shader_animation: default_true(),
762 cursor_shader_animation_speed: default_custom_shader_speed(),
763 cursor_shader_color: default_cursor_shader_color(),
764 cursor_shader_trail_duration: default_cursor_trail_duration(),
765 cursor_shader_glow_radius: default_cursor_glow_radius(),
766 cursor_shader_glow_intensity: default_cursor_glow_intensity(),
767 cursor_shader_hides_cursor: default_false(),
768 exit_on_shell_exit: default_true(),
769 custom_shell: None,
770 shell_args: None,
771 working_directory: None,
772 shell_env: None,
773 login_shell: default_login_shell(),
774 scrollbar_position: default_scrollbar_position(),
775 scrollbar_width: default_scrollbar_width(),
776 scrollbar_thumb_color: default_scrollbar_thumb_color(),
777 scrollbar_track_color: default_scrollbar_track_color(),
778 clipboard_max_sync_events: default_clipboard_max_sync_events(),
779 clipboard_max_event_bytes: default_clipboard_max_event_bytes(),
780 notification_bell_desktop: default_false(),
781 notification_bell_sound: default_bell_sound(),
782 notification_bell_visual: default_true(),
783 notification_activity_enabled: default_false(),
784 notification_activity_threshold: default_activity_threshold(),
785 notification_silence_enabled: default_false(),
786 notification_silence_threshold: default_silence_threshold(),
787 notification_max_buffer: default_notification_max_buffer(),
788 tab_bar_mode: TabBarMode::default(),
789 tab_bar_height: default_tab_bar_height(),
790 tab_show_close_button: default_true(),
791 tab_show_index: default_false(),
792 tab_inherit_cwd: default_true(),
793 max_tabs: default_zero(),
794 }
795 }
796}
797
798impl Config {
799 #[allow(dead_code)]
801 pub fn new() -> Self {
802 Self::default()
803 }
804
805 pub fn load() -> Result<Self> {
807 let config_path = Self::config_path();
808 log::info!("Config path: {:?}", config_path);
809
810 if config_path.exists() {
811 log::info!("Loading existing config from {:?}", config_path);
812 let contents = fs::read_to_string(&config_path)?;
813 let config: Config = serde_yaml::from_str(&contents)?;
814 Ok(config)
815 } else {
816 log::info!(
817 "Config file not found, creating default at {:?}",
818 config_path
819 );
820 let config = Self::default();
822 if let Err(e) = config.save() {
823 log::error!("Failed to save default config: {}", e);
824 return Err(e);
825 }
826 log::info!("Default config created successfully");
827 Ok(config)
828 }
829 }
830
831 pub fn save(&self) -> Result<()> {
833 let config_path = Self::config_path();
834
835 if let Some(parent) = config_path.parent() {
837 fs::create_dir_all(parent)?;
838 }
839
840 let yaml = serde_yaml::to_string(self)?;
841 fs::write(&config_path, yaml)?;
842
843 Ok(())
844 }
845
846 pub fn config_path() -> PathBuf {
848 #[cfg(target_os = "windows")]
849 {
850 if let Some(config_dir) = dirs::config_dir() {
851 config_dir.join("par-term").join("config.yaml")
852 } else {
853 PathBuf::from("config.yaml")
854 }
855 }
856 #[cfg(not(target_os = "windows"))]
857 {
858 if let Some(home_dir) = dirs::home_dir() {
860 home_dir
861 .join(".config")
862 .join("par-term")
863 .join("config.yaml")
864 } else {
865 PathBuf::from("config.yaml")
867 }
868 }
869 }
870
871 pub fn shaders_dir() -> PathBuf {
873 #[cfg(target_os = "windows")]
874 {
875 if let Some(config_dir) = dirs::config_dir() {
876 config_dir.join("par-term").join("shaders")
877 } else {
878 PathBuf::from("shaders")
879 }
880 }
881 #[cfg(not(target_os = "windows"))]
882 {
883 if let Some(home_dir) = dirs::home_dir() {
884 home_dir.join(".config").join("par-term").join("shaders")
885 } else {
886 PathBuf::from("shaders")
887 }
888 }
889 }
890
891 pub fn shader_path(shader_name: &str) -> PathBuf {
895 let path = PathBuf::from(shader_name);
896 if path.is_absolute() {
897 path
898 } else {
899 Self::shaders_dir().join(shader_name)
900 }
901 }
902
903 pub fn resolve_texture_path(path: &str) -> PathBuf {
906 if path.starts_with("~/")
907 && let Some(home) = dirs::home_dir()
908 {
909 return home.join(&path[2..]);
910 }
911 PathBuf::from(path)
912 }
913
914 pub fn shader_channel_paths(&self) -> [Option<PathBuf>; 4] {
917 [
918 self.custom_shader_channel1
919 .as_ref()
920 .map(|p| Self::resolve_texture_path(p)),
921 self.custom_shader_channel2
922 .as_ref()
923 .map(|p| Self::resolve_texture_path(p)),
924 self.custom_shader_channel3
925 .as_ref()
926 .map(|p| Self::resolve_texture_path(p)),
927 self.custom_shader_channel4
928 .as_ref()
929 .map(|p| Self::resolve_texture_path(p)),
930 ]
931 }
932
933 #[allow(dead_code)]
935 pub fn with_dimensions(mut self, cols: usize, rows: usize) -> Self {
936 self.cols = cols;
937 self.rows = rows;
938 self
939 }
940
941 #[allow(dead_code)]
943 pub fn with_font_size(mut self, size: f32) -> Self {
944 self.font_size = size;
945 self
946 }
947
948 #[allow(dead_code)]
950 pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
951 self.font_family = family.into();
952 self
953 }
954
955 #[allow(dead_code)]
957 pub fn with_title(mut self, title: impl Into<String>) -> Self {
958 self.window_title = title.into();
959 self
960 }
961
962 #[allow(dead_code)]
964 pub fn with_scrollback(mut self, size: usize) -> Self {
965 self.scrollback_lines = size;
966 self
967 }
968
969 pub fn load_theme(&self) -> Theme {
971 Theme::by_name(&self.theme).unwrap_or_default()
972 }
973}