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, Serialize, Deserialize)]
64pub struct FontRange {
65 pub start: u32,
67 pub end: u32,
69 pub font_family: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Config {
77 #[serde(default = "default_cols")]
82 pub cols: usize,
83
84 #[serde(default = "default_rows")]
86 pub rows: usize,
87
88 #[serde(default = "default_font_size")]
90 pub font_size: f32,
91
92 #[serde(default = "default_font_family")]
94 pub font_family: String,
95
96 #[serde(default)]
98 pub font_family_bold: Option<String>,
99
100 #[serde(default)]
102 pub font_family_italic: Option<String>,
103
104 #[serde(default)]
106 pub font_family_bold_italic: Option<String>,
107
108 #[serde(default)]
112 pub font_ranges: Vec<FontRange>,
113
114 #[serde(default = "default_line_spacing")]
116 pub line_spacing: f32,
117
118 #[serde(default = "default_char_spacing")]
120 pub char_spacing: f32,
121
122 #[serde(default = "default_text_shaping")]
125 pub enable_text_shaping: bool,
126
127 #[serde(default = "default_true")]
129 pub enable_ligatures: bool,
130
131 #[serde(default = "default_true")]
133 pub enable_kerning: bool,
134
135 #[serde(default = "default_window_title")]
137 pub window_title: String,
138
139 #[serde(default = "default_true")]
142 pub allow_title_change: bool,
143
144 #[serde(default = "default_max_fps", alias = "refresh_rate")]
150 pub max_fps: u32,
151
152 #[serde(default)]
159 pub vsync_mode: VsyncMode,
160
161 #[serde(default = "default_window_padding")]
163 pub window_padding: f32,
164
165 #[serde(default = "default_window_opacity")]
167 pub window_opacity: f32,
168
169 #[serde(default = "default_false")]
171 pub window_always_on_top: bool,
172
173 #[serde(default = "default_true")]
175 pub window_decorations: bool,
176
177 #[serde(default = "default_window_width")]
179 pub window_width: u32,
180
181 #[serde(default = "default_window_height")]
183 pub window_height: u32,
184
185 #[serde(default)]
187 pub background_image: Option<String>,
188
189 #[serde(default = "default_true")]
191 pub background_image_enabled: bool,
192
193 #[serde(default)]
200 pub background_image_mode: BackgroundImageMode,
201
202 #[serde(default = "default_background_image_opacity")]
204 pub background_image_opacity: f32,
205
206 #[serde(default)]
210 pub custom_shader: Option<String>,
211
212 #[serde(default = "default_true")]
214 pub custom_shader_enabled: bool,
215
216 #[serde(default = "default_true")]
219 pub custom_shader_animation: bool,
220
221 #[serde(default = "default_custom_shader_speed")]
223 pub custom_shader_animation_speed: f32,
224
225 #[serde(default = "default_text_opacity")]
228 pub custom_shader_text_opacity: f32,
229
230 #[serde(default = "default_false")]
234 pub custom_shader_full_content: bool,
235
236 #[serde(default)]
242 pub cursor_shader: Option<String>,
243
244 #[serde(default = "default_false")]
246 pub cursor_shader_enabled: bool,
247
248 #[serde(default = "default_true")]
250 pub cursor_shader_animation: bool,
251
252 #[serde(default = "default_custom_shader_speed")]
254 pub cursor_shader_animation_speed: f32,
255
256 #[serde(default = "default_cursor_shader_color")]
259 pub cursor_shader_color: [u8; 3],
260
261 #[serde(default = "default_cursor_trail_duration")]
264 pub cursor_shader_trail_duration: f32,
265
266 #[serde(default = "default_cursor_glow_radius")]
269 pub cursor_shader_glow_radius: f32,
270
271 #[serde(default = "default_cursor_glow_intensity")]
274 pub cursor_shader_glow_intensity: f32,
275
276 #[serde(default = "default_true")]
281 pub auto_copy_selection: bool,
282
283 #[serde(default = "default_false", alias = "strip_trailing_newline_on_copy")]
286 pub copy_trailing_newline: bool,
287
288 #[serde(default = "default_true")]
290 pub middle_click_paste: bool,
291
292 #[serde(default = "default_scroll_speed")]
297 pub mouse_scroll_speed: f32,
298
299 #[serde(default = "default_double_click_threshold")]
301 pub mouse_double_click_threshold: u64,
302
303 #[serde(default = "default_triple_click_threshold")]
305 pub mouse_triple_click_threshold: u64,
306
307 #[serde(default = "default_scrollback", alias = "scrollback_size")]
312 pub scrollback_lines: usize,
313
314 #[serde(default = "default_false")]
316 pub cursor_blink: bool,
317
318 #[serde(default = "default_cursor_blink_interval")]
320 pub cursor_blink_interval: u64,
321
322 #[serde(default)]
324 pub cursor_style: CursorStyle,
325
326 #[serde(default = "default_cursor_color")]
328 pub cursor_color: [u8; 3],
329
330 #[serde(default = "default_scrollbar_autohide_delay")]
335 pub scrollbar_autohide_delay: u64,
336
337 #[serde(default = "default_theme")]
342 pub theme: String,
343
344 #[serde(default = "default_screenshot_format")]
349 pub screenshot_format: String,
350
351 #[serde(default = "default_true", alias = "close_on_shell_exit")]
356 pub exit_on_shell_exit: bool,
357
358 #[serde(default)]
360 pub custom_shell: Option<String>,
361
362 #[serde(default)]
364 pub shell_args: Option<Vec<String>>,
365
366 #[serde(default)]
368 pub working_directory: Option<String>,
369
370 #[serde(default)]
372 pub shell_env: Option<std::collections::HashMap<String, String>>,
373
374 #[serde(default = "default_login_shell")]
378 pub login_shell: bool,
379
380 #[serde(default = "default_scrollbar_position")]
385 pub scrollbar_position: String,
386
387 #[serde(default = "default_scrollbar_width")]
389 pub scrollbar_width: f32,
390
391 #[serde(default = "default_scrollbar_thumb_color")]
393 pub scrollbar_thumb_color: [f32; 4],
394
395 #[serde(default = "default_scrollbar_track_color")]
397 pub scrollbar_track_color: [f32; 4],
398
399 #[serde(
404 default = "default_clipboard_max_sync_events",
405 alias = "max_clipboard_sync_events"
406 )]
407 pub clipboard_max_sync_events: usize,
408
409 #[serde(
411 default = "default_clipboard_max_event_bytes",
412 alias = "max_clipboard_event_bytes"
413 )]
414 pub clipboard_max_event_bytes: usize,
415
416 #[serde(default = "default_false", alias = "bell_desktop")]
421 pub notification_bell_desktop: bool,
422
423 #[serde(default = "default_bell_sound", alias = "bell_sound")]
425 pub notification_bell_sound: u8,
426
427 #[serde(default = "default_true", alias = "bell_visual")]
429 pub notification_bell_visual: bool,
430
431 #[serde(default = "default_false", alias = "activity_notifications")]
433 pub notification_activity_enabled: bool,
434
435 #[serde(default = "default_activity_threshold", alias = "activity_threshold")]
437 pub notification_activity_threshold: u64,
438
439 #[serde(default = "default_false", alias = "silence_notifications")]
441 pub notification_silence_enabled: bool,
442
443 #[serde(default = "default_silence_threshold", alias = "silence_threshold")]
445 pub notification_silence_threshold: u64,
446
447 #[serde(
449 default = "default_notification_max_buffer",
450 alias = "max_notifications"
451 )]
452 pub notification_max_buffer: usize,
453}
454
455fn default_cols() -> usize {
457 80
458}
459
460fn default_rows() -> usize {
461 24
462}
463
464fn default_font_size() -> f32 {
465 13.0
466}
467
468fn default_font_family() -> String {
469 "JetBrains Mono".to_string()
470}
471
472fn default_line_spacing() -> f32 {
473 1.0 }
475
476fn default_char_spacing() -> f32 {
477 1.0 }
479
480fn default_text_shaping() -> bool {
481 true }
483
484fn default_scrollback() -> usize {
485 10000
486}
487
488fn default_window_title() -> String {
489 "par-term".to_string()
490}
491
492fn default_theme() -> String {
493 "dark-background".to_string()
494}
495
496fn default_screenshot_format() -> String {
497 "png".to_string()
498}
499
500fn default_max_fps() -> u32 {
501 60
502}
503
504fn default_window_padding() -> f32 {
505 10.0
506}
507
508fn default_login_shell() -> bool {
509 true
510}
511
512fn default_scrollbar_position() -> String {
513 "right".to_string()
514}
515
516fn default_scrollbar_width() -> f32 {
517 15.0
518}
519
520fn default_scrollbar_thumb_color() -> [f32; 4] {
521 [0.4, 0.4, 0.4, 0.95] }
523
524fn default_scrollbar_track_color() -> [f32; 4] {
525 [0.15, 0.15, 0.15, 0.6] }
527
528fn default_clipboard_max_sync_events() -> usize {
529 64 }
531
532fn default_clipboard_max_event_bytes() -> usize {
533 2048 }
535
536fn default_activity_threshold() -> u64 {
537 10 }
539
540fn default_silence_threshold() -> u64 {
541 300 }
543
544fn default_notification_max_buffer() -> usize {
545 64 }
547
548fn default_scroll_speed() -> f32 {
549 3.0 }
551
552fn default_double_click_threshold() -> u64 {
553 500 }
555
556fn default_triple_click_threshold() -> u64 {
557 500 }
559
560fn default_cursor_blink_interval() -> u64 {
561 500 }
563
564fn default_cursor_color() -> [u8; 3] {
565 [255, 255, 255] }
567
568fn default_scrollbar_autohide_delay() -> u64 {
569 0 }
571
572fn default_window_opacity() -> f32 {
573 1.0 }
575
576fn default_window_width() -> u32 {
577 1600 }
579
580fn default_window_height() -> u32 {
581 600 }
583
584fn default_background_image_opacity() -> f32 {
585 1.0 }
587
588fn default_false() -> bool {
589 false
590}
591
592fn default_true() -> bool {
593 true
594}
595
596fn default_text_opacity() -> f32 {
597 1.0 }
599
600fn default_custom_shader_speed() -> f32 {
601 1.0 }
603
604fn default_cursor_shader_color() -> [u8; 3] {
605 [255, 255, 255] }
607
608fn default_cursor_trail_duration() -> f32 {
609 0.5 }
611
612fn default_cursor_glow_radius() -> f32 {
613 80.0 }
615
616fn default_cursor_glow_intensity() -> f32 {
617 0.3 }
619
620fn default_bell_sound() -> u8 {
621 50 }
623
624impl Default for Config {
625 fn default() -> Self {
626 Self {
627 cols: default_cols(),
628 rows: default_rows(),
629 font_size: default_font_size(),
630 font_family: default_font_family(),
631 font_family_bold: None,
632 font_family_italic: None,
633 font_family_bold_italic: None,
634 font_ranges: Vec::new(),
635 line_spacing: default_line_spacing(),
636 char_spacing: default_char_spacing(),
637 enable_text_shaping: default_text_shaping(),
638 enable_ligatures: default_true(),
639 enable_kerning: default_true(),
640 scrollback_lines: default_scrollback(),
641 cursor_blink: default_false(),
642 cursor_blink_interval: default_cursor_blink_interval(),
643 cursor_style: CursorStyle::default(),
644 cursor_color: default_cursor_color(),
645 scrollbar_autohide_delay: default_scrollbar_autohide_delay(),
646 window_title: default_window_title(),
647 allow_title_change: default_true(),
648 theme: default_theme(),
649 auto_copy_selection: default_true(),
650 copy_trailing_newline: default_false(),
651 middle_click_paste: default_true(),
652 mouse_scroll_speed: default_scroll_speed(),
653 mouse_double_click_threshold: default_double_click_threshold(),
654 mouse_triple_click_threshold: default_triple_click_threshold(),
655 screenshot_format: default_screenshot_format(),
656 max_fps: default_max_fps(),
657 vsync_mode: VsyncMode::default(),
658 window_padding: default_window_padding(),
659 window_opacity: default_window_opacity(),
660 window_always_on_top: default_false(),
661 window_decorations: default_true(),
662 window_width: default_window_width(),
663 window_height: default_window_height(),
664 background_image: None,
665 background_image_enabled: default_true(),
666 background_image_mode: BackgroundImageMode::default(),
667 background_image_opacity: default_background_image_opacity(),
668 custom_shader: None,
669 custom_shader_enabled: default_true(),
670 custom_shader_animation: default_true(),
671 custom_shader_animation_speed: default_custom_shader_speed(),
672 custom_shader_text_opacity: default_text_opacity(),
673 custom_shader_full_content: default_false(),
674 cursor_shader: None,
675 cursor_shader_enabled: default_false(),
676 cursor_shader_animation: default_true(),
677 cursor_shader_animation_speed: default_custom_shader_speed(),
678 cursor_shader_color: default_cursor_shader_color(),
679 cursor_shader_trail_duration: default_cursor_trail_duration(),
680 cursor_shader_glow_radius: default_cursor_glow_radius(),
681 cursor_shader_glow_intensity: default_cursor_glow_intensity(),
682 exit_on_shell_exit: default_true(),
683 custom_shell: None,
684 shell_args: None,
685 working_directory: None,
686 shell_env: None,
687 login_shell: default_login_shell(),
688 scrollbar_position: default_scrollbar_position(),
689 scrollbar_width: default_scrollbar_width(),
690 scrollbar_thumb_color: default_scrollbar_thumb_color(),
691 scrollbar_track_color: default_scrollbar_track_color(),
692 clipboard_max_sync_events: default_clipboard_max_sync_events(),
693 clipboard_max_event_bytes: default_clipboard_max_event_bytes(),
694 notification_bell_desktop: default_false(),
695 notification_bell_sound: default_bell_sound(),
696 notification_bell_visual: default_true(),
697 notification_activity_enabled: default_false(),
698 notification_activity_threshold: default_activity_threshold(),
699 notification_silence_enabled: default_false(),
700 notification_silence_threshold: default_silence_threshold(),
701 notification_max_buffer: default_notification_max_buffer(),
702 }
703 }
704}
705
706impl Config {
707 #[allow(dead_code)]
709 pub fn new() -> Self {
710 Self::default()
711 }
712
713 pub fn load() -> Result<Self> {
715 let config_path = Self::config_path();
716 log::info!("Config path: {:?}", config_path);
717
718 if config_path.exists() {
719 log::info!("Loading existing config from {:?}", config_path);
720 let contents = fs::read_to_string(&config_path)?;
721 let config: Config = serde_yaml::from_str(&contents)?;
722 Ok(config)
723 } else {
724 log::info!(
725 "Config file not found, creating default at {:?}",
726 config_path
727 );
728 let config = Self::default();
730 if let Err(e) = config.save() {
731 log::error!("Failed to save default config: {}", e);
732 return Err(e);
733 }
734 log::info!("Default config created successfully");
735 Ok(config)
736 }
737 }
738
739 pub fn save(&self) -> Result<()> {
741 let config_path = Self::config_path();
742
743 if let Some(parent) = config_path.parent() {
745 fs::create_dir_all(parent)?;
746 }
747
748 let yaml = serde_yaml::to_string(self)?;
749 fs::write(&config_path, yaml)?;
750
751 Ok(())
752 }
753
754 pub fn config_path() -> PathBuf {
756 #[cfg(target_os = "windows")]
757 {
758 if let Some(config_dir) = dirs::config_dir() {
759 config_dir.join("par-term").join("config.yaml")
760 } else {
761 PathBuf::from("config.yaml")
762 }
763 }
764 #[cfg(not(target_os = "windows"))]
765 {
766 if let Some(home_dir) = dirs::home_dir() {
768 home_dir
769 .join(".config")
770 .join("par-term")
771 .join("config.yaml")
772 } else {
773 PathBuf::from("config.yaml")
775 }
776 }
777 }
778
779 pub fn shaders_dir() -> PathBuf {
781 #[cfg(target_os = "windows")]
782 {
783 if let Some(config_dir) = dirs::config_dir() {
784 config_dir.join("par-term").join("shaders")
785 } else {
786 PathBuf::from("shaders")
787 }
788 }
789 #[cfg(not(target_os = "windows"))]
790 {
791 if let Some(home_dir) = dirs::home_dir() {
792 home_dir.join(".config").join("par-term").join("shaders")
793 } else {
794 PathBuf::from("shaders")
795 }
796 }
797 }
798
799 pub fn shader_path(shader_name: &str) -> PathBuf {
803 let path = PathBuf::from(shader_name);
804 if path.is_absolute() {
805 path
806 } else {
807 Self::shaders_dir().join(shader_name)
808 }
809 }
810
811 #[allow(dead_code)]
813 pub fn with_dimensions(mut self, cols: usize, rows: usize) -> Self {
814 self.cols = cols;
815 self.rows = rows;
816 self
817 }
818
819 #[allow(dead_code)]
821 pub fn with_font_size(mut self, size: f32) -> Self {
822 self.font_size = size;
823 self
824 }
825
826 #[allow(dead_code)]
828 pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
829 self.font_family = family.into();
830 self
831 }
832
833 #[allow(dead_code)]
835 pub fn with_title(mut self, title: impl Into<String>) -> Self {
836 self.window_title = title.into();
837 self
838 }
839
840 #[allow(dead_code)]
842 pub fn with_scrollback(mut self, size: usize) -> Self {
843 self.scrollback_lines = size;
844 self
845 }
846
847 pub fn load_theme(&self) -> Theme {
849 Theme::by_name(&self.theme).unwrap_or_default()
850 }
851}