par_term/
config.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6/// VSync mode (presentation mode)
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
8#[serde(rename_all = "lowercase")]
9pub enum VsyncMode {
10    /// No VSync - render as fast as possible (lowest latency, highest GPU usage)
11    #[default]
12    Immediate,
13    /// Mailbox VSync - cap at monitor refresh rate with triple buffering (balanced)
14    Mailbox,
15    /// FIFO VSync - strict vsync with double buffering (lowest GPU usage, slight input lag)
16    Fifo,
17}
18
19impl VsyncMode {
20    /// Convert to wgpu::PresentMode
21    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/// Cursor style
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
34#[serde(rename_all = "lowercase")]
35pub enum CursorStyle {
36    /// Block cursor (fills entire cell)
37    #[default]
38    Block,
39    /// Beam cursor (vertical line at cell start)
40    Beam,
41    /// Underline cursor (horizontal line at cell bottom)
42    Underline,
43}
44
45/// Background image display mode
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
47#[serde(rename_all = "lowercase")]
48pub enum BackgroundImageMode {
49    /// Scale to fit window while maintaining aspect ratio (may have letterboxing)
50    Fit,
51    /// Scale to fill window while maintaining aspect ratio (may crop edges)
52    Fill,
53    /// Stretch to fill window exactly (ignores aspect ratio)
54    #[default]
55    Stretch,
56    /// Repeat image in a tiled pattern at original size
57    Tile,
58    /// Center image at original size (no scaling)
59    Center,
60}
61
62/// Font mapping for a specific Unicode range
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct FontRange {
65    /// Start of Unicode range (inclusive), e.g., 0x4E00 for CJK
66    pub start: u32,
67    /// End of Unicode range (inclusive), e.g., 0x9FFF for CJK
68    pub end: u32,
69    /// Font family name to use for this range
70    pub font_family: String,
71}
72
73/// Configuration for the terminal emulator
74/// Aligned with par-tui-term naming conventions for consistency
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Config {
77    // ========================================================================
78    // Window & Display (GUI-specific)
79    // ========================================================================
80    /// Number of columns in the terminal
81    #[serde(default = "default_cols")]
82    pub cols: usize,
83
84    /// Number of rows in the terminal
85    #[serde(default = "default_rows")]
86    pub rows: usize,
87
88    /// Font size in points
89    #[serde(default = "default_font_size")]
90    pub font_size: f32,
91
92    /// Font family name (regular/normal weight)
93    #[serde(default = "default_font_family")]
94    pub font_family: String,
95
96    /// Bold font family name (optional, defaults to font_family)
97    #[serde(default)]
98    pub font_family_bold: Option<String>,
99
100    /// Italic font family name (optional, defaults to font_family)
101    #[serde(default)]
102    pub font_family_italic: Option<String>,
103
104    /// Bold italic font family name (optional, defaults to font_family)
105    #[serde(default)]
106    pub font_family_bold_italic: Option<String>,
107
108    /// Custom font mappings for specific Unicode ranges
109    /// Format: Vec of (start_codepoint, end_codepoint, font_family_name)
110    /// Example: [(0x4E00, 0x9FFF, "Noto Sans CJK SC")] for CJK Unified Ideographs
111    #[serde(default)]
112    pub font_ranges: Vec<FontRange>,
113
114    /// Line height multiplier (1.0 = tight, 1.2 = default, 1.5 = spacious)
115    #[serde(default = "default_line_spacing")]
116    pub line_spacing: f32,
117
118    /// Character width multiplier (0.5 = narrow, 0.6 = default, 0.7 = wide)
119    #[serde(default = "default_char_spacing")]
120    pub char_spacing: f32,
121
122    /// Enable text shaping for ligatures and complex scripts
123    /// When enabled, uses HarfBuzz for proper ligature, emoji, and complex script rendering
124    #[serde(default = "default_text_shaping")]
125    pub enable_text_shaping: bool,
126
127    /// Enable ligatures (requires enable_text_shaping)
128    #[serde(default = "default_true")]
129    pub enable_ligatures: bool,
130
131    /// Enable kerning adjustments (requires enable_text_shaping)
132    #[serde(default = "default_true")]
133    pub enable_kerning: bool,
134
135    /// Window title
136    #[serde(default = "default_window_title")]
137    pub window_title: String,
138
139    /// Allow applications to change the window title via OSC escape sequences
140    /// When false, the window title will always be the configured window_title
141    #[serde(default = "default_true")]
142    pub allow_title_change: bool,
143
144    /// Maximum frames per second (FPS) target
145    /// Controls how frequently the terminal requests screen redraws.
146    /// Note: On macOS, actual FPS may be lower (~22-25) due to system-level
147    /// VSync throttling in wgpu/Metal, regardless of this setting.
148    /// Default: 60
149    #[serde(default = "default_max_fps", alias = "refresh_rate")]
150    pub max_fps: u32,
151
152    /// VSync mode - controls GPU frame synchronization
153    /// - immediate: No VSync, render as fast as possible (lowest latency, highest power)
154    /// - mailbox: Cap at monitor refresh rate with triple buffering (balanced)
155    /// - fifo: Strict VSync with double buffering (lowest power, slight input lag)
156    ///
157    /// Default: immediate (for maximum performance)
158    #[serde(default)]
159    pub vsync_mode: VsyncMode,
160
161    /// Window padding in pixels
162    #[serde(default = "default_window_padding")]
163    pub window_padding: f32,
164
165    /// Window opacity/transparency (0.0 = fully transparent, 1.0 = fully opaque)
166    #[serde(default = "default_window_opacity")]
167    pub window_opacity: f32,
168
169    /// Keep window always on top of other windows
170    #[serde(default = "default_false")]
171    pub window_always_on_top: bool,
172
173    /// Show window decorations (title bar, borders)
174    #[serde(default = "default_true")]
175    pub window_decorations: bool,
176
177    /// Initial window width in pixels
178    #[serde(default = "default_window_width")]
179    pub window_width: u32,
180
181    /// Initial window height in pixels
182    #[serde(default = "default_window_height")]
183    pub window_height: u32,
184
185    /// Background image path (optional, supports ~ for home directory)
186    #[serde(default)]
187    pub background_image: Option<String>,
188
189    /// Enable or disable background image rendering (even if a path is set)
190    #[serde(default = "default_true")]
191    pub background_image_enabled: bool,
192
193    /// Background image display mode
194    /// - fit: Scale to fit window while maintaining aspect ratio (default)
195    /// - fill: Scale to fill window while maintaining aspect ratio (may crop)
196    /// - stretch: Stretch to fill window (ignores aspect ratio)
197    /// - tile: Repeat image in a tiled pattern
198    /// - center: Center image at original size
199    #[serde(default)]
200    pub background_image_mode: BackgroundImageMode,
201
202    /// Background image opacity (0.0 = fully transparent, 1.0 = fully opaque)
203    #[serde(default = "default_background_image_opacity")]
204    pub background_image_opacity: f32,
205
206    /// Custom shader file path (GLSL format, relative to shaders folder or absolute)
207    /// Shaders are loaded from ~/.config/par-term/shaders/ by default
208    /// Supports Ghostty/Shadertoy-style GLSL shaders with iTime, iResolution, iChannel0
209    #[serde(default)]
210    pub custom_shader: Option<String>,
211
212    /// Enable or disable the custom shader (even if a path is set)
213    #[serde(default = "default_true")]
214    pub custom_shader_enabled: bool,
215
216    /// Enable animation in custom shader (updates iTime uniform each frame)
217    /// When disabled, iTime is fixed at 0.0 for static effects
218    #[serde(default = "default_true")]
219    pub custom_shader_animation: bool,
220
221    /// Animation speed multiplier for custom shader (1.0 = normal speed)
222    #[serde(default = "default_custom_shader_speed")]
223    pub custom_shader_animation_speed: f32,
224
225    /// Text opacity when using custom shader (0.0 = transparent, 1.0 = fully opaque)
226    /// This allows text to remain readable while the shader effect shows through the background
227    #[serde(default = "default_text_opacity")]
228    pub custom_shader_text_opacity: f32,
229
230    /// When enabled, the shader receives the full rendered terminal content (text + background)
231    /// and can manipulate/distort it. When disabled (default), the shader only provides
232    /// a background and text is composited on top cleanly.
233    #[serde(default = "default_false")]
234    pub custom_shader_full_content: bool,
235
236    // ========================================================================
237    // Cursor Shader Settings (separate from background shader)
238    // ========================================================================
239    /// Cursor shader file path (GLSL format, relative to shaders folder or absolute)
240    /// This is a separate shader specifically for cursor effects (trails, glows, etc.)
241    #[serde(default)]
242    pub cursor_shader: Option<String>,
243
244    /// Enable or disable the cursor shader (even if a path is set)
245    #[serde(default = "default_false")]
246    pub cursor_shader_enabled: bool,
247
248    /// Enable animation in cursor shader (updates iTime uniform each frame)
249    #[serde(default = "default_true")]
250    pub cursor_shader_animation: bool,
251
252    /// Animation speed multiplier for cursor shader (1.0 = normal speed)
253    #[serde(default = "default_custom_shader_speed")]
254    pub cursor_shader_animation_speed: f32,
255
256    /// Cursor color for shader effects [R, G, B] (0-255)
257    /// This color is passed to the shader via iCursorShaderColor uniform
258    #[serde(default = "default_cursor_shader_color")]
259    pub cursor_shader_color: [u8; 3],
260
261    /// Duration of cursor trail effect in seconds
262    /// Passed to shader via iCursorTrailDuration uniform
263    #[serde(default = "default_cursor_trail_duration")]
264    pub cursor_shader_trail_duration: f32,
265
266    /// Radius of cursor glow effect in pixels
267    /// Passed to shader via iCursorGlowRadius uniform
268    #[serde(default = "default_cursor_glow_radius")]
269    pub cursor_shader_glow_radius: f32,
270
271    /// Intensity of cursor glow effect (0.0 = none, 1.0 = full)
272    /// Passed to shader via iCursorGlowIntensity uniform
273    #[serde(default = "default_cursor_glow_intensity")]
274    pub cursor_shader_glow_intensity: f32,
275
276    // ========================================================================
277    // Selection & Clipboard
278    // ========================================================================
279    /// Automatically copy selected text to clipboard
280    #[serde(default = "default_true")]
281    pub auto_copy_selection: bool,
282
283    /// Include trailing newline when copying lines
284    /// Note: Inverted logic from old strip_trailing_newline_on_copy
285    #[serde(default = "default_false", alias = "strip_trailing_newline_on_copy")]
286    pub copy_trailing_newline: bool,
287
288    /// Paste on middle mouse button click
289    #[serde(default = "default_true")]
290    pub middle_click_paste: bool,
291
292    // ========================================================================
293    // Mouse Behavior
294    // ========================================================================
295    /// Mouse wheel scroll speed multiplier
296    #[serde(default = "default_scroll_speed")]
297    pub mouse_scroll_speed: f32,
298
299    /// Double-click timing threshold in milliseconds
300    #[serde(default = "default_double_click_threshold")]
301    pub mouse_double_click_threshold: u64,
302
303    /// Triple-click timing threshold in milliseconds (typically same as double-click)
304    #[serde(default = "default_triple_click_threshold")]
305    pub mouse_triple_click_threshold: u64,
306
307    // ========================================================================
308    // Scrollback & Cursor
309    // ========================================================================
310    /// Maximum number of lines to keep in scrollback buffer
311    #[serde(default = "default_scrollback", alias = "scrollback_size")]
312    pub scrollback_lines: usize,
313
314    /// Enable cursor blinking
315    #[serde(default = "default_false")]
316    pub cursor_blink: bool,
317
318    /// Cursor blink interval in milliseconds
319    #[serde(default = "default_cursor_blink_interval")]
320    pub cursor_blink_interval: u64,
321
322    /// Cursor style (block, beam, underline)
323    #[serde(default)]
324    pub cursor_style: CursorStyle,
325
326    /// Cursor color [R, G, B] (0-255)
327    #[serde(default = "default_cursor_color")]
328    pub cursor_color: [u8; 3],
329
330    // ========================================================================
331    // Scrollbar
332    // ========================================================================
333    /// Auto-hide scrollbar after inactivity (milliseconds, 0 = never hide)
334    #[serde(default = "default_scrollbar_autohide_delay")]
335    pub scrollbar_autohide_delay: u64,
336
337    // ========================================================================
338    // Theme & Colors
339    // ========================================================================
340    /// Color theme name to use for terminal colors
341    #[serde(default = "default_theme")]
342    pub theme: String,
343
344    // ========================================================================
345    // Screenshot
346    // ========================================================================
347    /// File format for screenshots (png, jpeg, svg, html)
348    #[serde(default = "default_screenshot_format")]
349    pub screenshot_format: String,
350
351    // ========================================================================
352    // Shell Behavior
353    // ========================================================================
354    /// Exit when shell exits
355    #[serde(default = "default_true", alias = "close_on_shell_exit")]
356    pub exit_on_shell_exit: bool,
357
358    /// Custom shell command (defaults to system shell if not specified)
359    #[serde(default)]
360    pub custom_shell: Option<String>,
361
362    /// Arguments to pass to the shell
363    #[serde(default)]
364    pub shell_args: Option<Vec<String>>,
365
366    /// Working directory for the shell (defaults to current directory)
367    #[serde(default)]
368    pub working_directory: Option<String>,
369
370    /// Environment variables to set for the shell
371    #[serde(default)]
372    pub shell_env: Option<std::collections::HashMap<String, String>>,
373
374    /// Whether to spawn the shell as a login shell (passes -l flag)
375    /// This is important on macOS to properly initialize PATH from Homebrew, /etc/paths.d, etc.
376    /// Default: true
377    #[serde(default = "default_login_shell")]
378    pub login_shell: bool,
379
380    // ========================================================================
381    // Scrollbar (GUI-specific)
382    // ========================================================================
383    /// Scrollbar position (left or right)
384    #[serde(default = "default_scrollbar_position")]
385    pub scrollbar_position: String,
386
387    /// Scrollbar width in pixels
388    #[serde(default = "default_scrollbar_width")]
389    pub scrollbar_width: f32,
390
391    /// Scrollbar thumb color (RGBA: [r, g, b, a] where each is 0.0-1.0)
392    #[serde(default = "default_scrollbar_thumb_color")]
393    pub scrollbar_thumb_color: [f32; 4],
394
395    /// Scrollbar track color (RGBA: [r, g, b, a] where each is 0.0-1.0)
396    #[serde(default = "default_scrollbar_track_color")]
397    pub scrollbar_track_color: [f32; 4],
398
399    // ========================================================================
400    // Clipboard Sync Limits
401    // ========================================================================
402    /// Maximum clipboard sync events retained for diagnostics
403    #[serde(
404        default = "default_clipboard_max_sync_events",
405        alias = "max_clipboard_sync_events"
406    )]
407    pub clipboard_max_sync_events: usize,
408
409    /// Maximum bytes stored per clipboard sync event
410    #[serde(
411        default = "default_clipboard_max_event_bytes",
412        alias = "max_clipboard_event_bytes"
413    )]
414    pub clipboard_max_event_bytes: usize,
415
416    // ========================================================================
417    // Notifications
418    // ========================================================================
419    /// Forward BEL events to desktop notification centers
420    #[serde(default = "default_false", alias = "bell_desktop")]
421    pub notification_bell_desktop: bool,
422
423    /// Volume (0-100) for backend bell sound alerts (0 disables)
424    #[serde(default = "default_bell_sound", alias = "bell_sound")]
425    pub notification_bell_sound: u8,
426
427    /// Enable backend visual bell overlay
428    #[serde(default = "default_true", alias = "bell_visual")]
429    pub notification_bell_visual: bool,
430
431    /// Enable notifications when activity resumes after inactivity
432    #[serde(default = "default_false", alias = "activity_notifications")]
433    pub notification_activity_enabled: bool,
434
435    /// Seconds of inactivity required before an activity alert fires
436    #[serde(default = "default_activity_threshold", alias = "activity_threshold")]
437    pub notification_activity_threshold: u64,
438
439    /// Enable notifications after prolonged silence
440    #[serde(default = "default_false", alias = "silence_notifications")]
441    pub notification_silence_enabled: bool,
442
443    /// Seconds of silence before a silence alert fires
444    #[serde(default = "default_silence_threshold", alias = "silence_threshold")]
445    pub notification_silence_threshold: u64,
446
447    /// Maximum number of OSC 9/777 notification entries retained by backend
448    #[serde(
449        default = "default_notification_max_buffer",
450        alias = "max_notifications"
451    )]
452    pub notification_max_buffer: usize,
453}
454
455// Default value functions
456fn 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 // Default line height multiplier
474}
475
476fn default_char_spacing() -> f32 {
477    1.0 // Default character width multiplier
478}
479
480fn default_text_shaping() -> bool {
481    true // Enabled by default - OpenType features now properly configured via Feature::from_str()
482}
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] // Medium gray, nearly opaque
522}
523
524fn default_scrollbar_track_color() -> [f32; 4] {
525    [0.15, 0.15, 0.15, 0.6] // Dark gray, semi-transparent
526}
527
528fn default_clipboard_max_sync_events() -> usize {
529    64 // Aligned with sister project
530}
531
532fn default_clipboard_max_event_bytes() -> usize {
533    2048 // Aligned with sister project
534}
535
536fn default_activity_threshold() -> u64 {
537    10 // Aligned with sister project (10 seconds)
538}
539
540fn default_silence_threshold() -> u64 {
541    300 // 5 minutes
542}
543
544fn default_notification_max_buffer() -> usize {
545    64 // Aligned with sister project
546}
547
548fn default_scroll_speed() -> f32 {
549    3.0 // Lines per scroll tick
550}
551
552fn default_double_click_threshold() -> u64 {
553    500 // 500 milliseconds
554}
555
556fn default_triple_click_threshold() -> u64 {
557    500 // 500 milliseconds (same as double-click)
558}
559
560fn default_cursor_blink_interval() -> u64 {
561    500 // 500 milliseconds (blink twice per second)
562}
563
564fn default_cursor_color() -> [u8; 3] {
565    [255, 255, 255] // White cursor
566}
567
568fn default_scrollbar_autohide_delay() -> u64 {
569    0 // 0 = never auto-hide (always visible when scrollback exists)
570}
571
572fn default_window_opacity() -> f32 {
573    1.0 // Fully opaque by default
574}
575
576fn default_window_width() -> u32 {
577    1600 // Default initial width
578}
579
580fn default_window_height() -> u32 {
581    600 // Default initial height
582}
583
584fn default_background_image_opacity() -> f32 {
585    1.0 // Fully opaque by default
586}
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 // Fully opaque text by default
598}
599
600fn default_custom_shader_speed() -> f32 {
601    1.0 // Normal animation speed
602}
603
604fn default_cursor_shader_color() -> [u8; 3] {
605    [255, 255, 255] // White cursor for shader effects
606}
607
608fn default_cursor_trail_duration() -> f32 {
609    0.5 // 500ms trail duration
610}
611
612fn default_cursor_glow_radius() -> f32 {
613    80.0 // 80 pixel glow radius
614}
615
616fn default_cursor_glow_intensity() -> f32 {
617    0.3 // 30% glow intensity
618}
619
620fn default_bell_sound() -> u8 {
621    50 // Default to 50% volume
622}
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    /// Create a new configuration with default values
708    #[allow(dead_code)]
709    pub fn new() -> Self {
710        Self::default()
711    }
712
713    /// Load configuration from file or create default
714    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            // Create default config and save it
729            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    /// Save configuration to file
740    pub fn save(&self) -> Result<()> {
741        let config_path = Self::config_path();
742
743        // Create parent directory if it doesn't exist
744        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    /// Get the configuration file path (using XDG convention)
755    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            // Use XDG convention on all platforms: ~/.config/par-term/config.yaml
767            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                // Fallback if home directory cannot be determined
774                PathBuf::from("config.yaml")
775            }
776        }
777    }
778
779    /// Get the shaders directory path (using XDG convention)
780    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    /// Get the full path to a shader file
800    /// If the shader path is absolute, returns it as-is
801    /// Otherwise, resolves it relative to the shaders directory
802    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    /// Set window dimensions
812    #[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    /// Set font size
820    #[allow(dead_code)]
821    pub fn with_font_size(mut self, size: f32) -> Self {
822        self.font_size = size;
823        self
824    }
825
826    /// Set font family
827    #[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    /// Set the window title
834    #[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    /// Set the scrollback buffer size
841    #[allow(dead_code)]
842    pub fn with_scrollback(mut self, size: usize) -> Self {
843        self.scrollback_lines = size;
844        self
845    }
846
847    /// Load theme configuration
848    pub fn load_theme(&self) -> Theme {
849        Theme::by_name(&self.theme).unwrap_or_default()
850    }
851}