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 = "default_true")]
241 pub auto_copy_selection: bool,
242
243 #[serde(default = "default_false", alias = "strip_trailing_newline_on_copy")]
246 pub copy_trailing_newline: bool,
247
248 #[serde(default = "default_true")]
250 pub middle_click_paste: bool,
251
252 #[serde(default = "default_scroll_speed")]
257 pub mouse_scroll_speed: f32,
258
259 #[serde(default = "default_double_click_threshold")]
261 pub mouse_double_click_threshold: u64,
262
263 #[serde(default = "default_triple_click_threshold")]
265 pub mouse_triple_click_threshold: u64,
266
267 #[serde(default = "default_scrollback", alias = "scrollback_size")]
272 pub scrollback_lines: usize,
273
274 #[serde(default = "default_false")]
276 pub cursor_blink: bool,
277
278 #[serde(default = "default_cursor_blink_interval")]
280 pub cursor_blink_interval: u64,
281
282 #[serde(default)]
284 pub cursor_style: CursorStyle,
285
286 #[serde(default = "default_scrollbar_autohide_delay")]
291 pub scrollbar_autohide_delay: u64,
292
293 #[serde(default = "default_theme")]
298 pub theme: String,
299
300 #[serde(default = "default_screenshot_format")]
305 pub screenshot_format: String,
306
307 #[serde(default = "default_true", alias = "close_on_shell_exit")]
312 pub exit_on_shell_exit: bool,
313
314 #[serde(default)]
316 pub custom_shell: Option<String>,
317
318 #[serde(default)]
320 pub shell_args: Option<Vec<String>>,
321
322 #[serde(default)]
324 pub working_directory: Option<String>,
325
326 #[serde(default)]
328 pub shell_env: Option<std::collections::HashMap<String, String>>,
329
330 #[serde(default = "default_scrollbar_position")]
335 pub scrollbar_position: String,
336
337 #[serde(default = "default_scrollbar_width")]
339 pub scrollbar_width: f32,
340
341 #[serde(default = "default_scrollbar_thumb_color")]
343 pub scrollbar_thumb_color: [f32; 4],
344
345 #[serde(default = "default_scrollbar_track_color")]
347 pub scrollbar_track_color: [f32; 4],
348
349 #[serde(
354 default = "default_clipboard_max_sync_events",
355 alias = "max_clipboard_sync_events"
356 )]
357 pub clipboard_max_sync_events: usize,
358
359 #[serde(
361 default = "default_clipboard_max_event_bytes",
362 alias = "max_clipboard_event_bytes"
363 )]
364 pub clipboard_max_event_bytes: usize,
365
366 #[serde(default = "default_false", alias = "bell_desktop")]
371 pub notification_bell_desktop: bool,
372
373 #[serde(default = "default_bell_sound", alias = "bell_sound")]
375 pub notification_bell_sound: u8,
376
377 #[serde(default = "default_true", alias = "bell_visual")]
379 pub notification_bell_visual: bool,
380
381 #[serde(default = "default_false", alias = "activity_notifications")]
383 pub notification_activity_enabled: bool,
384
385 #[serde(default = "default_activity_threshold", alias = "activity_threshold")]
387 pub notification_activity_threshold: u64,
388
389 #[serde(default = "default_false", alias = "silence_notifications")]
391 pub notification_silence_enabled: bool,
392
393 #[serde(default = "default_silence_threshold", alias = "silence_threshold")]
395 pub notification_silence_threshold: u64,
396
397 #[serde(
399 default = "default_notification_max_buffer",
400 alias = "max_notifications"
401 )]
402 pub notification_max_buffer: usize,
403}
404
405fn default_cols() -> usize {
407 80
408}
409
410fn default_rows() -> usize {
411 24
412}
413
414fn default_font_size() -> f32 {
415 13.0
416}
417
418fn default_font_family() -> String {
419 "JetBrains Mono".to_string()
420}
421
422fn default_line_spacing() -> f32 {
423 1.0 }
425
426fn default_char_spacing() -> f32 {
427 1.0 }
429
430fn default_text_shaping() -> bool {
431 true }
433
434fn default_scrollback() -> usize {
435 10000
436}
437
438fn default_window_title() -> String {
439 "par-term".to_string()
440}
441
442fn default_theme() -> String {
443 "dark-background".to_string()
444}
445
446fn default_screenshot_format() -> String {
447 "png".to_string()
448}
449
450fn default_max_fps() -> u32 {
451 60
452}
453
454fn default_window_padding() -> f32 {
455 10.0
456}
457
458fn default_scrollbar_position() -> String {
459 "right".to_string()
460}
461
462fn default_scrollbar_width() -> f32 {
463 15.0
464}
465
466fn default_scrollbar_thumb_color() -> [f32; 4] {
467 [0.4, 0.4, 0.4, 0.95] }
469
470fn default_scrollbar_track_color() -> [f32; 4] {
471 [0.15, 0.15, 0.15, 0.6] }
473
474fn default_clipboard_max_sync_events() -> usize {
475 64 }
477
478fn default_clipboard_max_event_bytes() -> usize {
479 2048 }
481
482fn default_activity_threshold() -> u64 {
483 10 }
485
486fn default_silence_threshold() -> u64 {
487 300 }
489
490fn default_notification_max_buffer() -> usize {
491 64 }
493
494fn default_scroll_speed() -> f32 {
495 3.0 }
497
498fn default_double_click_threshold() -> u64 {
499 500 }
501
502fn default_triple_click_threshold() -> u64 {
503 500 }
505
506fn default_cursor_blink_interval() -> u64 {
507 500 }
509
510fn default_scrollbar_autohide_delay() -> u64 {
511 0 }
513
514fn default_window_opacity() -> f32 {
515 1.0 }
517
518fn default_window_width() -> u32 {
519 1600 }
521
522fn default_window_height() -> u32 {
523 600 }
525
526fn default_background_image_opacity() -> f32 {
527 1.0 }
529
530fn default_false() -> bool {
531 false
532}
533
534fn default_true() -> bool {
535 true
536}
537
538fn default_text_opacity() -> f32 {
539 1.0 }
541
542fn default_custom_shader_speed() -> f32 {
543 1.0 }
545
546fn default_bell_sound() -> u8 {
547 50 }
549
550impl Default for Config {
551 fn default() -> Self {
552 Self {
553 cols: default_cols(),
554 rows: default_rows(),
555 font_size: default_font_size(),
556 font_family: default_font_family(),
557 font_family_bold: None,
558 font_family_italic: None,
559 font_family_bold_italic: None,
560 font_ranges: Vec::new(),
561 line_spacing: default_line_spacing(),
562 char_spacing: default_char_spacing(),
563 enable_text_shaping: default_text_shaping(),
564 enable_ligatures: default_true(),
565 enable_kerning: default_true(),
566 scrollback_lines: default_scrollback(),
567 cursor_blink: default_false(),
568 cursor_blink_interval: default_cursor_blink_interval(),
569 cursor_style: CursorStyle::default(),
570 scrollbar_autohide_delay: default_scrollbar_autohide_delay(),
571 window_title: default_window_title(),
572 allow_title_change: default_true(),
573 theme: default_theme(),
574 auto_copy_selection: default_true(),
575 copy_trailing_newline: default_false(),
576 middle_click_paste: default_true(),
577 mouse_scroll_speed: default_scroll_speed(),
578 mouse_double_click_threshold: default_double_click_threshold(),
579 mouse_triple_click_threshold: default_triple_click_threshold(),
580 screenshot_format: default_screenshot_format(),
581 max_fps: default_max_fps(),
582 vsync_mode: VsyncMode::default(),
583 window_padding: default_window_padding(),
584 window_opacity: default_window_opacity(),
585 window_always_on_top: default_false(),
586 window_decorations: default_true(),
587 window_width: default_window_width(),
588 window_height: default_window_height(),
589 background_image: None,
590 background_image_enabled: default_true(),
591 background_image_mode: BackgroundImageMode::default(),
592 background_image_opacity: default_background_image_opacity(),
593 custom_shader: None,
594 custom_shader_enabled: default_true(),
595 custom_shader_animation: default_true(),
596 custom_shader_animation_speed: default_custom_shader_speed(),
597 custom_shader_text_opacity: default_text_opacity(),
598 custom_shader_full_content: default_false(),
599 exit_on_shell_exit: default_true(),
600 custom_shell: None,
601 shell_args: None,
602 working_directory: None,
603 shell_env: None,
604 scrollbar_position: default_scrollbar_position(),
605 scrollbar_width: default_scrollbar_width(),
606 scrollbar_thumb_color: default_scrollbar_thumb_color(),
607 scrollbar_track_color: default_scrollbar_track_color(),
608 clipboard_max_sync_events: default_clipboard_max_sync_events(),
609 clipboard_max_event_bytes: default_clipboard_max_event_bytes(),
610 notification_bell_desktop: default_false(),
611 notification_bell_sound: default_bell_sound(),
612 notification_bell_visual: default_true(),
613 notification_activity_enabled: default_false(),
614 notification_activity_threshold: default_activity_threshold(),
615 notification_silence_enabled: default_false(),
616 notification_silence_threshold: default_silence_threshold(),
617 notification_max_buffer: default_notification_max_buffer(),
618 }
619 }
620}
621
622impl Config {
623 #[allow(dead_code)]
625 pub fn new() -> Self {
626 Self::default()
627 }
628
629 pub fn load() -> Result<Self> {
631 let config_path = Self::config_path();
632 log::info!("Config path: {:?}", config_path);
633
634 if config_path.exists() {
635 log::info!("Loading existing config from {:?}", config_path);
636 let contents = fs::read_to_string(&config_path)?;
637 let config: Config = serde_yaml::from_str(&contents)?;
638 Ok(config)
639 } else {
640 log::info!(
641 "Config file not found, creating default at {:?}",
642 config_path
643 );
644 let config = Self::default();
646 if let Err(e) = config.save() {
647 log::error!("Failed to save default config: {}", e);
648 return Err(e);
649 }
650 log::info!("Default config created successfully");
651 Ok(config)
652 }
653 }
654
655 pub fn save(&self) -> Result<()> {
657 let config_path = Self::config_path();
658
659 if let Some(parent) = config_path.parent() {
661 fs::create_dir_all(parent)?;
662 }
663
664 let yaml = serde_yaml::to_string(self)?;
665 fs::write(&config_path, yaml)?;
666
667 Ok(())
668 }
669
670 pub fn config_path() -> PathBuf {
672 #[cfg(target_os = "windows")]
673 {
674 if let Some(config_dir) = dirs::config_dir() {
675 config_dir.join("par-term").join("config.yaml")
676 } else {
677 PathBuf::from("config.yaml")
678 }
679 }
680 #[cfg(not(target_os = "windows"))]
681 {
682 if let Some(home_dir) = dirs::home_dir() {
684 home_dir
685 .join(".config")
686 .join("par-term")
687 .join("config.yaml")
688 } else {
689 PathBuf::from("config.yaml")
691 }
692 }
693 }
694
695 pub fn shaders_dir() -> PathBuf {
697 #[cfg(target_os = "windows")]
698 {
699 if let Some(config_dir) = dirs::config_dir() {
700 config_dir.join("par-term").join("shaders")
701 } else {
702 PathBuf::from("shaders")
703 }
704 }
705 #[cfg(not(target_os = "windows"))]
706 {
707 if let Some(home_dir) = dirs::home_dir() {
708 home_dir.join(".config").join("par-term").join("shaders")
709 } else {
710 PathBuf::from("shaders")
711 }
712 }
713 }
714
715 pub fn shader_path(shader_name: &str) -> PathBuf {
719 let path = PathBuf::from(shader_name);
720 if path.is_absolute() {
721 path
722 } else {
723 Self::shaders_dir().join(shader_name)
724 }
725 }
726
727 #[allow(dead_code)]
729 pub fn with_dimensions(mut self, cols: usize, rows: usize) -> Self {
730 self.cols = cols;
731 self.rows = rows;
732 self
733 }
734
735 #[allow(dead_code)]
737 pub fn with_font_size(mut self, size: f32) -> Self {
738 self.font_size = size;
739 self
740 }
741
742 #[allow(dead_code)]
744 pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
745 self.font_family = family.into();
746 self
747 }
748
749 #[allow(dead_code)]
751 pub fn with_title(mut self, title: impl Into<String>) -> Self {
752 self.window_title = title.into();
753 self
754 }
755
756 #[allow(dead_code)]
758 pub fn with_scrollback(mut self, size: usize) -> Self {
759 self.scrollback_lines = size;
760 self
761 }
762
763 pub fn load_theme(&self) -> Theme {
765 Theme::by_name(&self.theme).unwrap_or_default()
766 }
767}