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/// Tab bar visibility mode
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
64#[serde(rename_all = "snake_case")]
65pub enum TabBarMode {
66    /// Always show tab bar
67    Always,
68    /// Show tab bar only when there are multiple tabs (default)
69    #[default]
70    WhenMultiple,
71    /// Never show tab bar
72    Never,
73}
74
75/// Font mapping for a specific Unicode range
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct FontRange {
78    /// Start of Unicode range (inclusive), e.g., 0x4E00 for CJK
79    pub start: u32,
80    /// End of Unicode range (inclusive), e.g., 0x9FFF for CJK
81    pub end: u32,
82    /// Font family name to use for this range
83    pub font_family: String,
84}
85
86/// Configuration for the terminal emulator
87/// Aligned with par-tui-term naming conventions for consistency
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct Config {
90    // ========================================================================
91    // Window & Display (GUI-specific)
92    // ========================================================================
93    /// Number of columns in the terminal
94    #[serde(default = "default_cols")]
95    pub cols: usize,
96
97    /// Number of rows in the terminal
98    #[serde(default = "default_rows")]
99    pub rows: usize,
100
101    /// Font size in points
102    #[serde(default = "default_font_size")]
103    pub font_size: f32,
104
105    /// Font family name (regular/normal weight)
106    #[serde(default = "default_font_family")]
107    pub font_family: String,
108
109    /// Bold font family name (optional, defaults to font_family)
110    #[serde(default)]
111    pub font_family_bold: Option<String>,
112
113    /// Italic font family name (optional, defaults to font_family)
114    #[serde(default)]
115    pub font_family_italic: Option<String>,
116
117    /// Bold italic font family name (optional, defaults to font_family)
118    #[serde(default)]
119    pub font_family_bold_italic: Option<String>,
120
121    /// Custom font mappings for specific Unicode ranges
122    /// Format: Vec of (start_codepoint, end_codepoint, font_family_name)
123    /// Example: [(0x4E00, 0x9FFF, "Noto Sans CJK SC")] for CJK Unified Ideographs
124    #[serde(default)]
125    pub font_ranges: Vec<FontRange>,
126
127    /// Line height multiplier (1.0 = tight, 1.2 = default, 1.5 = spacious)
128    #[serde(default = "default_line_spacing")]
129    pub line_spacing: f32,
130
131    /// Character width multiplier (0.5 = narrow, 0.6 = default, 0.7 = wide)
132    #[serde(default = "default_char_spacing")]
133    pub char_spacing: f32,
134
135    /// Enable text shaping for ligatures and complex scripts
136    /// When enabled, uses HarfBuzz for proper ligature, emoji, and complex script rendering
137    #[serde(default = "default_text_shaping")]
138    pub enable_text_shaping: bool,
139
140    /// Enable ligatures (requires enable_text_shaping)
141    #[serde(default = "default_true")]
142    pub enable_ligatures: bool,
143
144    /// Enable kerning adjustments (requires enable_text_shaping)
145    #[serde(default = "default_true")]
146    pub enable_kerning: bool,
147
148    /// Window title
149    #[serde(default = "default_window_title")]
150    pub window_title: String,
151
152    /// Allow applications to change the window title via OSC escape sequences
153    /// When false, the window title will always be the configured window_title
154    #[serde(default = "default_true")]
155    pub allow_title_change: bool,
156
157    /// Maximum frames per second (FPS) target
158    /// Controls how frequently the terminal requests screen redraws.
159    /// Note: On macOS, actual FPS may be lower (~22-25) due to system-level
160    /// VSync throttling in wgpu/Metal, regardless of this setting.
161    /// Default: 60
162    #[serde(default = "default_max_fps", alias = "refresh_rate")]
163    pub max_fps: u32,
164
165    /// VSync mode - controls GPU frame synchronization
166    /// - immediate: No VSync, render as fast as possible (lowest latency, highest power)
167    /// - mailbox: Cap at monitor refresh rate with triple buffering (balanced)
168    /// - fifo: Strict VSync with double buffering (lowest power, slight input lag)
169    ///
170    /// Default: immediate (for maximum performance)
171    #[serde(default)]
172    pub vsync_mode: VsyncMode,
173
174    /// Window padding in pixels
175    #[serde(default = "default_window_padding")]
176    pub window_padding: f32,
177
178    /// Window opacity/transparency (0.0 = fully transparent, 1.0 = fully opaque)
179    #[serde(default = "default_window_opacity")]
180    pub window_opacity: f32,
181
182    /// Keep window always on top of other windows
183    #[serde(default = "default_false")]
184    pub window_always_on_top: bool,
185
186    /// Show window decorations (title bar, borders)
187    #[serde(default = "default_true")]
188    pub window_decorations: bool,
189
190    /// Initial window width in pixels
191    #[serde(default = "default_window_width")]
192    pub window_width: u32,
193
194    /// Initial window height in pixels
195    #[serde(default = "default_window_height")]
196    pub window_height: u32,
197
198    /// Background image path (optional, supports ~ for home directory)
199    #[serde(default)]
200    pub background_image: Option<String>,
201
202    /// Enable or disable background image rendering (even if a path is set)
203    #[serde(default = "default_true")]
204    pub background_image_enabled: bool,
205
206    /// Background image display mode
207    /// - fit: Scale to fit window while maintaining aspect ratio (default)
208    /// - fill: Scale to fill window while maintaining aspect ratio (may crop)
209    /// - stretch: Stretch to fill window (ignores aspect ratio)
210    /// - tile: Repeat image in a tiled pattern
211    /// - center: Center image at original size
212    #[serde(default)]
213    pub background_image_mode: BackgroundImageMode,
214
215    /// Background image opacity (0.0 = fully transparent, 1.0 = fully opaque)
216    #[serde(default = "default_background_image_opacity")]
217    pub background_image_opacity: f32,
218
219    /// Custom shader file path (GLSL format, relative to shaders folder or absolute)
220    /// Shaders are loaded from ~/.config/par-term/shaders/ by default
221    /// Supports Ghostty/Shadertoy-style GLSL shaders with iTime, iResolution, iChannel0
222    #[serde(default)]
223    pub custom_shader: Option<String>,
224
225    /// Enable or disable the custom shader (even if a path is set)
226    #[serde(default = "default_true")]
227    pub custom_shader_enabled: bool,
228
229    /// Enable animation in custom shader (updates iTime uniform each frame)
230    /// When disabled, iTime is fixed at 0.0 for static effects
231    #[serde(default = "default_true")]
232    pub custom_shader_animation: bool,
233
234    /// Animation speed multiplier for custom shader (1.0 = normal speed)
235    #[serde(default = "default_custom_shader_speed")]
236    pub custom_shader_animation_speed: f32,
237
238    /// Text opacity when using custom shader (0.0 = transparent, 1.0 = fully opaque)
239    /// This allows text to remain readable while the shader effect shows through the background
240    #[serde(default = "default_text_opacity")]
241    pub custom_shader_text_opacity: f32,
242
243    /// When enabled, the shader receives the full rendered terminal content (text + background)
244    /// and can manipulate/distort it. When disabled (default), the shader only provides
245    /// a background and text is composited on top cleanly.
246    #[serde(default = "default_false")]
247    pub custom_shader_full_content: bool,
248
249    /// Brightness multiplier for custom shader output (0.05 = very dark, 1.0 = full brightness)
250    /// This dims the shader background to improve text readability
251    #[serde(default = "default_custom_shader_brightness")]
252    pub custom_shader_brightness: f32,
253
254    /// Texture file path for custom shader iChannel1 (optional)
255    /// Supports ~ for home directory. Example: "~/textures/noise.png"
256    #[serde(default)]
257    pub custom_shader_channel1: Option<String>,
258
259    /// Texture file path for custom shader iChannel2 (optional)
260    #[serde(default)]
261    pub custom_shader_channel2: Option<String>,
262
263    /// Texture file path for custom shader iChannel3 (optional)
264    #[serde(default)]
265    pub custom_shader_channel3: Option<String>,
266
267    /// Texture file path for custom shader iChannel4 (optional)
268    #[serde(default)]
269    pub custom_shader_channel4: Option<String>,
270
271    // ========================================================================
272    // Cursor Shader Settings (separate from background shader)
273    // ========================================================================
274    /// Cursor shader file path (GLSL format, relative to shaders folder or absolute)
275    /// This is a separate shader specifically for cursor effects (trails, glows, etc.)
276    #[serde(default)]
277    pub cursor_shader: Option<String>,
278
279    /// Enable or disable the cursor shader (even if a path is set)
280    #[serde(default = "default_false")]
281    pub cursor_shader_enabled: bool,
282
283    /// Enable animation in cursor shader (updates iTime uniform each frame)
284    #[serde(default = "default_true")]
285    pub cursor_shader_animation: bool,
286
287    /// Animation speed multiplier for cursor shader (1.0 = normal speed)
288    #[serde(default = "default_custom_shader_speed")]
289    pub cursor_shader_animation_speed: f32,
290
291    /// Cursor color for shader effects [R, G, B] (0-255)
292    /// This color is passed to the shader via iCursorShaderColor uniform
293    #[serde(default = "default_cursor_shader_color")]
294    pub cursor_shader_color: [u8; 3],
295
296    /// Duration of cursor trail effect in seconds
297    /// Passed to shader via iCursorTrailDuration uniform
298    #[serde(default = "default_cursor_trail_duration")]
299    pub cursor_shader_trail_duration: f32,
300
301    /// Radius of cursor glow effect in pixels
302    /// Passed to shader via iCursorGlowRadius uniform
303    #[serde(default = "default_cursor_glow_radius")]
304    pub cursor_shader_glow_radius: f32,
305
306    /// Intensity of cursor glow effect (0.0 = none, 1.0 = full)
307    /// Passed to shader via iCursorGlowIntensity uniform
308    #[serde(default = "default_cursor_glow_intensity")]
309    pub cursor_shader_glow_intensity: f32,
310
311    /// Hide the default cursor when cursor shader is enabled
312    /// When true and cursor_shader_enabled is true, the normal cursor is not drawn
313    /// This allows cursor shaders to fully replace the cursor rendering
314    #[serde(default = "default_false")]
315    pub cursor_shader_hides_cursor: bool,
316
317    // ========================================================================
318    // Selection & Clipboard
319    // ========================================================================
320    /// Automatically copy selected text to clipboard
321    #[serde(default = "default_true")]
322    pub auto_copy_selection: bool,
323
324    /// Include trailing newline when copying lines
325    /// Note: Inverted logic from old strip_trailing_newline_on_copy
326    #[serde(default = "default_false", alias = "strip_trailing_newline_on_copy")]
327    pub copy_trailing_newline: bool,
328
329    /// Paste on middle mouse button click
330    #[serde(default = "default_true")]
331    pub middle_click_paste: bool,
332
333    // ========================================================================
334    // Mouse Behavior
335    // ========================================================================
336    /// Mouse wheel scroll speed multiplier
337    #[serde(default = "default_scroll_speed")]
338    pub mouse_scroll_speed: f32,
339
340    /// Double-click timing threshold in milliseconds
341    #[serde(default = "default_double_click_threshold")]
342    pub mouse_double_click_threshold: u64,
343
344    /// Triple-click timing threshold in milliseconds (typically same as double-click)
345    #[serde(default = "default_triple_click_threshold")]
346    pub mouse_triple_click_threshold: u64,
347
348    // ========================================================================
349    // Scrollback & Cursor
350    // ========================================================================
351    /// Maximum number of lines to keep in scrollback buffer
352    #[serde(default = "default_scrollback", alias = "scrollback_size")]
353    pub scrollback_lines: usize,
354
355    /// Enable cursor blinking
356    #[serde(default = "default_false")]
357    pub cursor_blink: bool,
358
359    /// Cursor blink interval in milliseconds
360    #[serde(default = "default_cursor_blink_interval")]
361    pub cursor_blink_interval: u64,
362
363    /// Cursor style (block, beam, underline)
364    #[serde(default)]
365    pub cursor_style: CursorStyle,
366
367    /// Cursor color [R, G, B] (0-255)
368    #[serde(default = "default_cursor_color")]
369    pub cursor_color: [u8; 3],
370
371    // ========================================================================
372    // Scrollbar
373    // ========================================================================
374    /// Auto-hide scrollbar after inactivity (milliseconds, 0 = never hide)
375    #[serde(default = "default_scrollbar_autohide_delay")]
376    pub scrollbar_autohide_delay: u64,
377
378    // ========================================================================
379    // Theme & Colors
380    // ========================================================================
381    /// Color theme name to use for terminal colors
382    #[serde(default = "default_theme")]
383    pub theme: String,
384
385    // ========================================================================
386    // Screenshot
387    // ========================================================================
388    /// File format for screenshots (png, jpeg, svg, html)
389    #[serde(default = "default_screenshot_format")]
390    pub screenshot_format: String,
391
392    // ========================================================================
393    // Shell Behavior
394    // ========================================================================
395    /// Exit when shell exits
396    #[serde(default = "default_true", alias = "close_on_shell_exit")]
397    pub exit_on_shell_exit: bool,
398
399    /// Custom shell command (defaults to system shell if not specified)
400    #[serde(default)]
401    pub custom_shell: Option<String>,
402
403    /// Arguments to pass to the shell
404    #[serde(default)]
405    pub shell_args: Option<Vec<String>>,
406
407    /// Working directory for the shell (defaults to current directory)
408    #[serde(default)]
409    pub working_directory: Option<String>,
410
411    /// Environment variables to set for the shell
412    #[serde(default)]
413    pub shell_env: Option<std::collections::HashMap<String, String>>,
414
415    /// Whether to spawn the shell as a login shell (passes -l flag)
416    /// This is important on macOS to properly initialize PATH from Homebrew, /etc/paths.d, etc.
417    /// Default: true
418    #[serde(default = "default_login_shell")]
419    pub login_shell: bool,
420
421    // ========================================================================
422    // Scrollbar (GUI-specific)
423    // ========================================================================
424    /// Scrollbar position (left or right)
425    #[serde(default = "default_scrollbar_position")]
426    pub scrollbar_position: String,
427
428    /// Scrollbar width in pixels
429    #[serde(default = "default_scrollbar_width")]
430    pub scrollbar_width: f32,
431
432    /// Scrollbar thumb color (RGBA: [r, g, b, a] where each is 0.0-1.0)
433    #[serde(default = "default_scrollbar_thumb_color")]
434    pub scrollbar_thumb_color: [f32; 4],
435
436    /// Scrollbar track color (RGBA: [r, g, b, a] where each is 0.0-1.0)
437    #[serde(default = "default_scrollbar_track_color")]
438    pub scrollbar_track_color: [f32; 4],
439
440    // ========================================================================
441    // Clipboard Sync Limits
442    // ========================================================================
443    /// Maximum clipboard sync events retained for diagnostics
444    #[serde(
445        default = "default_clipboard_max_sync_events",
446        alias = "max_clipboard_sync_events"
447    )]
448    pub clipboard_max_sync_events: usize,
449
450    /// Maximum bytes stored per clipboard sync event
451    #[serde(
452        default = "default_clipboard_max_event_bytes",
453        alias = "max_clipboard_event_bytes"
454    )]
455    pub clipboard_max_event_bytes: usize,
456
457    // ========================================================================
458    // Notifications
459    // ========================================================================
460    /// Forward BEL events to desktop notification centers
461    #[serde(default = "default_false", alias = "bell_desktop")]
462    pub notification_bell_desktop: bool,
463
464    /// Volume (0-100) for backend bell sound alerts (0 disables)
465    #[serde(default = "default_bell_sound", alias = "bell_sound")]
466    pub notification_bell_sound: u8,
467
468    /// Enable backend visual bell overlay
469    #[serde(default = "default_true", alias = "bell_visual")]
470    pub notification_bell_visual: bool,
471
472    /// Enable notifications when activity resumes after inactivity
473    #[serde(default = "default_false", alias = "activity_notifications")]
474    pub notification_activity_enabled: bool,
475
476    /// Seconds of inactivity required before an activity alert fires
477    #[serde(default = "default_activity_threshold", alias = "activity_threshold")]
478    pub notification_activity_threshold: u64,
479
480    /// Enable notifications after prolonged silence
481    #[serde(default = "default_false", alias = "silence_notifications")]
482    pub notification_silence_enabled: bool,
483
484    /// Seconds of silence before a silence alert fires
485    #[serde(default = "default_silence_threshold", alias = "silence_threshold")]
486    pub notification_silence_threshold: u64,
487
488    /// Maximum number of OSC 9/777 notification entries retained by backend
489    #[serde(
490        default = "default_notification_max_buffer",
491        alias = "max_notifications"
492    )]
493    pub notification_max_buffer: usize,
494
495    // ========================================================================
496    // Tab Settings
497    // ========================================================================
498    /// Tab bar visibility mode (always, when_multiple, never)
499    #[serde(default)]
500    pub tab_bar_mode: TabBarMode,
501
502    /// Tab bar height in pixels
503    #[serde(default = "default_tab_bar_height")]
504    pub tab_bar_height: f32,
505
506    /// Show close button on tabs
507    #[serde(default = "default_true")]
508    pub tab_show_close_button: bool,
509
510    /// Show tab index numbers (for Cmd+1-9)
511    #[serde(default = "default_false")]
512    pub tab_show_index: bool,
513
514    /// New tab inherits working directory from active tab
515    #[serde(default = "default_true")]
516    pub tab_inherit_cwd: bool,
517
518    /// Maximum tabs per window (0 = unlimited)
519    #[serde(default = "default_zero")]
520    pub max_tabs: usize,
521}
522
523// Default value functions
524fn 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 // Default line height multiplier
542}
543
544fn default_char_spacing() -> f32 {
545    1.0 // Default character width multiplier
546}
547
548fn default_text_shaping() -> bool {
549    true // Enabled by default - OpenType features now properly configured via Feature::from_str()
550}
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] // Medium gray, nearly opaque
590}
591
592fn default_scrollbar_track_color() -> [f32; 4] {
593    [0.15, 0.15, 0.15, 0.6] // Dark gray, semi-transparent
594}
595
596fn default_clipboard_max_sync_events() -> usize {
597    64 // Aligned with sister project
598}
599
600fn default_clipboard_max_event_bytes() -> usize {
601    2048 // Aligned with sister project
602}
603
604fn default_activity_threshold() -> u64 {
605    10 // Aligned with sister project (10 seconds)
606}
607
608fn default_silence_threshold() -> u64 {
609    300 // 5 minutes
610}
611
612fn default_notification_max_buffer() -> usize {
613    64 // Aligned with sister project
614}
615
616fn default_scroll_speed() -> f32 {
617    3.0 // Lines per scroll tick
618}
619
620fn default_double_click_threshold() -> u64 {
621    500 // 500 milliseconds
622}
623
624fn default_triple_click_threshold() -> u64 {
625    500 // 500 milliseconds (same as double-click)
626}
627
628fn default_cursor_blink_interval() -> u64 {
629    500 // 500 milliseconds (blink twice per second)
630}
631
632fn default_cursor_color() -> [u8; 3] {
633    [255, 255, 255] // White cursor
634}
635
636fn default_scrollbar_autohide_delay() -> u64 {
637    0 // 0 = never auto-hide (always visible when scrollback exists)
638}
639
640fn default_window_opacity() -> f32 {
641    1.0 // Fully opaque by default
642}
643
644fn default_window_width() -> u32 {
645    1600 // Default initial width
646}
647
648fn default_window_height() -> u32 {
649    600 // Default initial height
650}
651
652fn default_background_image_opacity() -> f32 {
653    1.0 // Fully opaque by default
654}
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 // Fully opaque text by default
666}
667
668fn default_custom_shader_speed() -> f32 {
669    1.0 // Normal animation speed
670}
671
672fn default_custom_shader_brightness() -> f32 {
673    1.0 // Full brightness by default
674}
675
676fn default_cursor_shader_color() -> [u8; 3] {
677    [255, 255, 255] // White cursor for shader effects
678}
679
680fn default_cursor_trail_duration() -> f32 {
681    0.5 // 500ms trail duration
682}
683
684fn default_cursor_glow_radius() -> f32 {
685    80.0 // 80 pixel glow radius
686}
687
688fn default_cursor_glow_intensity() -> f32 {
689    0.3 // 30% glow intensity
690}
691
692fn default_bell_sound() -> u8 {
693    50 // Default to 50% volume
694}
695
696fn default_tab_bar_height() -> f32 {
697    28.0 // Default tab bar height in pixels
698}
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    /// Create a new configuration with default values
800    #[allow(dead_code)]
801    pub fn new() -> Self {
802        Self::default()
803    }
804
805    /// Load configuration from file or create default
806    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            // Create default config and save it
821            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    /// Save configuration to file
832    pub fn save(&self) -> Result<()> {
833        let config_path = Self::config_path();
834
835        // Create parent directory if it doesn't exist
836        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    /// Get the configuration file path (using XDG convention)
847    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            // Use XDG convention on all platforms: ~/.config/par-term/config.yaml
859            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                // Fallback if home directory cannot be determined
866                PathBuf::from("config.yaml")
867            }
868        }
869    }
870
871    /// Get the shaders directory path (using XDG convention)
872    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    /// Get the full path to a shader file
892    /// If the shader path is absolute, returns it as-is
893    /// Otherwise, resolves it relative to the shaders directory
894    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    /// Resolve a texture path, expanding ~ to home directory
904    /// Returns the expanded path or the original if expansion fails
905    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    /// Get the channel texture paths as an array of Options
915    /// Returns [channel1, channel2, channel3, channel4]
916    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    /// Set window dimensions
934    #[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    /// Set font size
942    #[allow(dead_code)]
943    pub fn with_font_size(mut self, size: f32) -> Self {
944        self.font_size = size;
945        self
946    }
947
948    /// Set font family
949    #[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    /// Set the window title
956    #[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    /// Set the scrollback buffer size
963    #[allow(dead_code)]
964    pub fn with_scrollback(mut self, size: usize) -> Self {
965        self.scrollback_lines = size;
966        self
967    }
968
969    /// Load theme configuration
970    pub fn load_theme(&self) -> Theme {
971        Theme::by_name(&self.theme).unwrap_or_default()
972    }
973}