Skip to main content

par_term_config/
config.rs

1//! Terminal configuration management.
2//!
3//! This module provides configuration loading, saving, and default values
4//! for the terminal emulator.
5
6use crate::snippets::{CustomActionConfig, SnippetConfig};
7use crate::themes::Theme;
8use crate::types::{
9    AlertEvent, AlertSoundConfig, BackgroundImageMode, BackgroundMode, CursorShaderConfig,
10    CursorStyle, DividerStyle, DownloadSaveLocation, DroppedFileQuoteStyle, FontRange,
11    ImageScalingMode, InstallPromptState, IntegrationVersions, KeyBinding, LogLevel,
12    ModifierRemapping, OptionKeyMode, PaneBackgroundConfig, PaneTitlePosition, PowerPreference,
13    ProgressBarPosition, ProgressBarStyle, SemanticHistoryEditorMode, SessionLogFormat,
14    ShaderConfig, ShaderInstallPrompt, ShellExitAction, SmartSelectionRule, StartupDirectoryMode,
15    StatusBarPosition, TabBarMode, TabBarPosition, TabStyle, ThinStrokesMode, UnfocusedCursorStyle,
16    UpdateCheckFrequency, VsyncMode, WindowType, default_smart_selection_rules,
17};
18
19use anyhow::Result;
20use regex::Regex;
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::fs;
24use std::path::PathBuf;
25
26/// Substitute `${VAR_NAME}` patterns in a string with environment variable values.
27///
28/// - `${VAR}` is replaced with the value of the environment variable `VAR`.
29/// - If the variable is not set, the `${VAR}` placeholder is left unchanged.
30/// - `$${VAR}` (doubled dollar sign) is an escape and produces the literal `${VAR}`.
31/// - Supports `${VAR:-default}` syntax for providing a default value when the variable is unset.
32///
33/// This is applied to the raw YAML config string before deserialization, so all
34/// string-typed config values benefit from substitution.
35pub fn substitute_variables(input: &str) -> String {
36    // First, replace escaped `$${` with a placeholder that won't match the regex
37    let escaped_placeholder = "\x00ESC_DOLLAR\x00";
38    let working = input.replace("$${", escaped_placeholder);
39
40    // Match ${VAR_NAME} or ${VAR_NAME:-default_value}
41    let re = Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-((?:[^}\\]|\\.)*))?}")
42        .expect("invalid regex");
43
44    let result = re.replace_all(&working, |caps: &regex::Captures| {
45        let var_name = &caps[1];
46        match std::env::var(var_name) {
47            Ok(val) => val,
48            Err(_) => {
49                // Use default value if provided, otherwise leave the placeholder as-is
50                caps.get(2)
51                    .map(|m| m.as_str().replace("\\}", "}"))
52                    .unwrap_or_else(|| caps[0].to_string())
53            }
54        }
55    });
56
57    // Restore escaped dollar signs
58    result.replace(escaped_placeholder, "${")
59}
60
61/// Custom deserializer for `ShellExitAction` that supports backward compatibility.
62///
63/// Accepts either:
64/// - Boolean: `true` → `Close`, `false` → `Keep` (legacy format)
65/// - String enum: `"close"`, `"keep"`, `"restart_immediately"`, etc.
66fn deserialize_shell_exit_action<'de, D>(deserializer: D) -> Result<ShellExitAction, D::Error>
67where
68    D: serde::Deserializer<'de>,
69{
70    #[derive(Deserialize)]
71    #[serde(untagged)]
72    enum BoolOrAction {
73        Bool(bool),
74        Action(ShellExitAction),
75    }
76
77    match BoolOrAction::deserialize(deserializer)? {
78        BoolOrAction::Bool(true) => Ok(ShellExitAction::Close),
79        BoolOrAction::Bool(false) => Ok(ShellExitAction::Keep),
80        BoolOrAction::Action(action) => Ok(action),
81    }
82}
83
84/// Configuration for the terminal emulator
85/// Aligned with par-tui-term naming conventions for consistency
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Config {
88    // ========================================================================
89    // Window & Display (GUI-specific)
90    // ========================================================================
91    /// Number of columns in the terminal
92    #[serde(default = "crate::defaults::cols")]
93    pub cols: usize,
94
95    /// Number of rows in the terminal
96    #[serde(default = "crate::defaults::rows")]
97    pub rows: usize,
98
99    /// Font size in points
100    #[serde(default = "crate::defaults::font_size")]
101    pub font_size: f32,
102
103    /// Font family name (regular/normal weight)
104    #[serde(default = "crate::defaults::font_family")]
105    pub font_family: String,
106
107    /// Bold font family name (optional, defaults to font_family)
108    #[serde(default)]
109    pub font_family_bold: Option<String>,
110
111    /// Italic font family name (optional, defaults to font_family)
112    #[serde(default)]
113    pub font_family_italic: Option<String>,
114
115    /// Bold italic font family name (optional, defaults to font_family)
116    #[serde(default)]
117    pub font_family_bold_italic: Option<String>,
118
119    /// Custom font mappings for specific Unicode ranges
120    /// Format: Vec of (start_codepoint, end_codepoint, font_family_name)
121    /// Example: [(0x4E00, 0x9FFF, "Noto Sans CJK SC")] for CJK Unified Ideographs
122    #[serde(default)]
123    pub font_ranges: Vec<FontRange>,
124
125    /// Line height multiplier (1.0 = tight, 1.2 = default, 1.5 = spacious)
126    #[serde(default = "crate::defaults::line_spacing")]
127    pub line_spacing: f32,
128
129    /// Character width multiplier (0.5 = narrow, 0.6 = default, 0.7 = wide)
130    #[serde(default = "crate::defaults::char_spacing")]
131    pub char_spacing: f32,
132
133    /// Enable text shaping for ligatures and complex scripts
134    /// When enabled, uses HarfBuzz for proper ligature, emoji, and complex script rendering
135    #[serde(default = "crate::defaults::text_shaping")]
136    pub enable_text_shaping: bool,
137
138    /// Enable ligatures (requires enable_text_shaping)
139    #[serde(default = "crate::defaults::bool_true")]
140    pub enable_ligatures: bool,
141
142    /// Enable kerning adjustments (requires enable_text_shaping)
143    #[serde(default = "crate::defaults::bool_true")]
144    pub enable_kerning: bool,
145
146    /// Enable anti-aliasing for font rendering
147    /// When false, text is rendered without smoothing (aliased/pixelated)
148    #[serde(default = "crate::defaults::bool_true")]
149    pub font_antialias: bool,
150
151    /// Enable hinting for font rendering
152    /// Hinting improves text clarity at small sizes by aligning glyphs to pixel boundaries
153    /// Disable for a softer, more "true to design" appearance
154    #[serde(default)]
155    pub font_hinting: bool,
156
157    /// Thin strokes / font smoothing mode
158    /// Controls stroke weight adjustment for improved rendering on different displays.
159    /// - never: Standard stroke weight everywhere
160    /// - retina_only: Lighter strokes on HiDPI displays (default)
161    /// - dark_backgrounds_only: Lighter strokes on dark backgrounds
162    /// - retina_dark_backgrounds_only: Lighter strokes only on HiDPI + dark backgrounds
163    /// - always: Always use lighter strokes
164    #[serde(default)]
165    pub font_thin_strokes: ThinStrokesMode,
166
167    /// Minimum contrast ratio for text against background (WCAG standard)
168    /// When set, adjusts foreground colors to ensure they meet the specified contrast ratio.
169    /// - 1.0: No adjustment (disabled)
170    /// - 4.5: WCAG AA standard for normal text
171    /// - 7.0: WCAG AAA standard for normal text
172    ///
173    /// Range: 1.0 to 21.0 (maximum possible contrast)
174    #[serde(default = "crate::defaults::minimum_contrast")]
175    pub minimum_contrast: f32,
176
177    /// Window title
178    #[serde(default = "crate::defaults::window_title")]
179    pub window_title: String,
180
181    /// Allow applications to change the window title via OSC escape sequences
182    /// When false, the window title will always be the configured window_title
183    #[serde(default = "crate::defaults::bool_true")]
184    pub allow_title_change: bool,
185
186    /// Maximum frames per second (FPS) target
187    /// Controls how frequently the terminal requests screen redraws.
188    /// Note: On macOS, actual FPS may be lower (~22-25) due to system-level
189    /// VSync throttling in wgpu/Metal, regardless of this setting.
190    /// Default: 60
191    #[serde(default = "crate::defaults::max_fps", alias = "refresh_rate")]
192    pub max_fps: u32,
193
194    /// VSync mode - controls GPU frame synchronization
195    /// - immediate: No VSync, render as fast as possible (lowest latency, highest power)
196    /// - mailbox: Cap at monitor refresh rate with triple buffering (balanced)
197    /// - fifo: Strict VSync with double buffering (lowest power, slight input lag)
198    ///
199    /// Default: immediate (for maximum performance)
200    #[serde(default)]
201    pub vsync_mode: VsyncMode,
202
203    /// GPU power preference for adapter selection
204    /// - none: Let the system decide (default)
205    /// - low_power: Prefer integrated GPU (saves battery)
206    /// - high_performance: Prefer discrete GPU (maximum performance)
207    ///
208    /// Note: Requires app restart to take effect.
209    #[serde(default)]
210    pub power_preference: PowerPreference,
211
212    /// Reduce flicker by delaying redraws while cursor is hidden (DECTCEM off).
213    /// Many terminal programs hide cursor during bulk updates to prevent visual artifacts.
214    #[serde(default = "crate::defaults::reduce_flicker")]
215    pub reduce_flicker: bool,
216
217    /// Maximum delay in milliseconds when reduce_flicker is enabled.
218    /// Rendering occurs when cursor becomes visible OR this delay expires.
219    /// Range: 1-100ms. Default: 16ms (~1 frame at 60fps).
220    #[serde(default = "crate::defaults::reduce_flicker_delay_ms")]
221    pub reduce_flicker_delay_ms: u32,
222
223    /// Enable throughput mode to batch rendering during bulk output.
224    /// When enabled, rendering is throttled to reduce CPU overhead for large outputs.
225    /// Toggle with Cmd+Shift+T (macOS) or Ctrl+Shift+T (other platforms).
226    #[serde(default = "crate::defaults::maximize_throughput")]
227    pub maximize_throughput: bool,
228
229    /// Render interval in milliseconds when maximize_throughput is enabled.
230    /// Higher values = better throughput but delayed display. Range: 50-500ms.
231    #[serde(default = "crate::defaults::throughput_render_interval_ms")]
232    pub throughput_render_interval_ms: u32,
233
234    /// Window padding in pixels
235    #[serde(default = "crate::defaults::window_padding")]
236    pub window_padding: f32,
237
238    /// Automatically hide window padding when panes are split
239    /// When true (default), window padding becomes 0 when the active tab has multiple panes
240    #[serde(default = "crate::defaults::bool_true")]
241    pub hide_window_padding_on_split: bool,
242
243    /// Window opacity/transparency (0.0 = fully transparent, 1.0 = fully opaque)
244    #[serde(default = "crate::defaults::window_opacity")]
245    pub window_opacity: f32,
246
247    /// Keep window always on top of other windows
248    #[serde(default = "crate::defaults::bool_false")]
249    pub window_always_on_top: bool,
250
251    /// Show window decorations (title bar, borders)
252    #[serde(default = "crate::defaults::bool_true")]
253    pub window_decorations: bool,
254
255    /// Window type (normal, fullscreen, or edge-anchored)
256    /// - normal: Standard window (default)
257    /// - fullscreen: Start in fullscreen mode
258    /// - edge_top/edge_bottom/edge_left/edge_right: Edge-anchored dropdown-style window
259    #[serde(default)]
260    pub window_type: WindowType,
261
262    /// Target monitor index for window placement (0 = primary)
263    /// Use None to let the OS decide window placement
264    #[serde(default)]
265    pub target_monitor: Option<usize>,
266
267    /// Target macOS Space (virtual desktop) for window placement (1-based ordinal)
268    /// Use None to let the OS decide which Space to open on.
269    /// Only effective on macOS; ignored on other platforms.
270    #[serde(default)]
271    pub target_space: Option<u32>,
272
273    /// Lock window size to prevent resize
274    /// When true, the window cannot be resized by the user
275    #[serde(default = "crate::defaults::bool_false")]
276    pub lock_window_size: bool,
277
278    /// Show window number in title bar
279    /// Useful when multiple par-term windows are open
280    #[serde(default = "crate::defaults::bool_false")]
281    pub show_window_number: bool,
282
283    /// When true, only the default terminal background is transparent.
284    /// Colored backgrounds (syntax highlighting, status bars, etc.) remain opaque.
285    /// This keeps text readable while allowing window transparency.
286    #[serde(default = "crate::defaults::bool_true")]
287    pub transparency_affects_only_default_background: bool,
288
289    /// When true, text is always rendered at full opacity regardless of window transparency.
290    /// This ensures text remains crisp and readable even with transparent backgrounds.
291    #[serde(default = "crate::defaults::bool_true")]
292    pub keep_text_opaque: bool,
293
294    /// Enable window blur effect (macOS only)
295    /// Blurs content behind the transparent window for better readability
296    #[serde(default = "crate::defaults::bool_false")]
297    pub blur_enabled: bool,
298
299    /// Blur radius in points (0-64, macOS only)
300    /// Higher values = more blur. Default: 10
301    #[serde(default = "crate::defaults::blur_radius")]
302    pub blur_radius: u32,
303
304    /// Background image path (optional, supports ~ for home directory)
305    #[serde(default)]
306    pub background_image: Option<String>,
307
308    /// Enable or disable background image rendering (even if a path is set)
309    #[serde(default = "crate::defaults::bool_true")]
310    pub background_image_enabled: bool,
311
312    /// Background image display mode
313    /// - fit: Scale to fit window while maintaining aspect ratio (default)
314    /// - fill: Scale to fill window while maintaining aspect ratio (may crop)
315    /// - stretch: Stretch to fill window (ignores aspect ratio)
316    /// - tile: Repeat image in a tiled pattern
317    /// - center: Center image at original size
318    #[serde(default)]
319    pub background_image_mode: BackgroundImageMode,
320
321    /// Background image opacity (0.0 = fully transparent, 1.0 = fully opaque)
322    #[serde(default = "crate::defaults::background_image_opacity")]
323    pub background_image_opacity: f32,
324
325    // ========================================================================
326    // Inline Image Settings (Sixel, iTerm2, Kitty)
327    // ========================================================================
328    /// Scaling quality for inline images (nearest = sharp/pixel art, linear = smooth)
329    #[serde(default)]
330    pub image_scaling_mode: ImageScalingMode,
331
332    /// Preserve aspect ratio when scaling inline images to fit terminal cells
333    #[serde(default = "crate::defaults::bool_true")]
334    pub image_preserve_aspect_ratio: bool,
335
336    /// Background mode selection (default, color, or image)
337    #[serde(default)]
338    pub background_mode: BackgroundMode,
339
340    /// Per-pane background image configurations
341    #[serde(default)]
342    pub pane_backgrounds: Vec<crate::config::PaneBackgroundConfig>,
343
344    /// Custom solid background color [R, G, B] (0-255)
345    /// Used when background_mode is "color"
346    /// Transparency is controlled by window_opacity
347    #[serde(default = "crate::defaults::background_color")]
348    pub background_color: [u8; 3],
349
350    // ========================================================================
351    // File Transfer Settings
352    // ========================================================================
353    /// Default save location for downloaded files
354    #[serde(default)]
355    pub download_save_location: DownloadSaveLocation,
356
357    /// Last used download directory (persisted internally)
358    #[serde(default, skip_serializing_if = "Option::is_none")]
359    pub last_download_directory: Option<String>,
360
361    /// Custom shader file path (GLSL format, relative to shaders folder or absolute)
362    /// Shaders are loaded from ~/.config/par-term/shaders/ by default
363    /// Supports Ghostty/Shadertoy-style GLSL shaders with iTime, iResolution, iChannel0-4
364    #[serde(default)]
365    pub custom_shader: Option<String>,
366
367    /// Enable or disable the custom shader (even if a path is set)
368    #[serde(default = "crate::defaults::bool_true")]
369    pub custom_shader_enabled: bool,
370
371    /// Enable animation in custom shader (updates iTime uniform each frame)
372    /// When disabled, iTime is fixed at 0.0 for static effects
373    #[serde(default = "crate::defaults::bool_true")]
374    pub custom_shader_animation: bool,
375
376    /// Animation speed multiplier for custom shader (1.0 = normal speed)
377    #[serde(default = "crate::defaults::custom_shader_speed")]
378    pub custom_shader_animation_speed: f32,
379
380    /// Text opacity when using custom shader (0.0 = transparent, 1.0 = fully opaque)
381    /// This allows text to remain readable while the shader effect shows through the background
382    #[serde(default = "crate::defaults::text_opacity")]
383    pub custom_shader_text_opacity: f32,
384
385    /// When enabled, the shader receives the full rendered terminal content (text + background)
386    /// and can manipulate/distort it. When disabled (default), the shader only provides
387    /// a background and text is composited on top cleanly.
388    #[serde(default = "crate::defaults::bool_false")]
389    pub custom_shader_full_content: bool,
390
391    /// Brightness multiplier for custom shader output (0.05 = very dark, 1.0 = full brightness)
392    /// This dims the shader background to improve text readability
393    #[serde(default = "crate::defaults::custom_shader_brightness")]
394    pub custom_shader_brightness: f32,
395
396    /// Texture file path for custom shader iChannel0 (optional, Shadertoy compatible)
397    /// Supports ~ for home directory. Example: "~/textures/noise.png"
398    #[serde(default)]
399    pub custom_shader_channel0: Option<String>,
400
401    /// Texture file path for custom shader iChannel1 (optional)
402    #[serde(default)]
403    pub custom_shader_channel1: Option<String>,
404
405    /// Texture file path for custom shader iChannel2 (optional)
406    #[serde(default)]
407    pub custom_shader_channel2: Option<String>,
408
409    /// Texture file path for custom shader iChannel3 (optional)
410    #[serde(default)]
411    pub custom_shader_channel3: Option<String>,
412
413    /// Cubemap texture path prefix for custom shaders (optional)
414    /// Expects 6 face files: {prefix}-px.{ext}, -nx.{ext}, -py.{ext}, -ny.{ext}, -pz.{ext}, -nz.{ext}
415    /// Supported formats: .png, .jpg, .jpeg, .hdr
416    /// Example: "textures/cubemaps/env-outside" will load env-outside-px.png, etc.
417    #[serde(default)]
418    pub custom_shader_cubemap: Option<String>,
419
420    /// Enable cubemap sampling in custom shaders
421    /// When enabled and a cubemap path is set, iCubemap uniform is available in shaders
422    #[serde(default = "crate::defaults::cubemap_enabled")]
423    pub custom_shader_cubemap_enabled: bool,
424
425    /// Use the app's background image as iChannel0 for custom shaders
426    /// When enabled, the configured background image is bound as iChannel0 instead of
427    /// the custom_shader_channel0 texture. This allows shaders to incorporate the
428    /// background image without requiring a separate texture file.
429    #[serde(default = "crate::defaults::use_background_as_channel0")]
430    pub custom_shader_use_background_as_channel0: bool,
431
432    // ========================================================================
433    // Cursor Shader Settings (separate from background shader)
434    // ========================================================================
435    /// Cursor shader file path (GLSL format, relative to shaders folder or absolute)
436    /// This is a separate shader specifically for cursor effects (trails, glows, etc.)
437    #[serde(default)]
438    pub cursor_shader: Option<String>,
439
440    /// Enable or disable the cursor shader (even if a path is set)
441    #[serde(default = "crate::defaults::bool_false")]
442    pub cursor_shader_enabled: bool,
443
444    /// Enable animation in cursor shader (updates iTime uniform each frame)
445    #[serde(default = "crate::defaults::bool_true")]
446    pub cursor_shader_animation: bool,
447
448    /// Animation speed multiplier for cursor shader (1.0 = normal speed)
449    #[serde(default = "crate::defaults::custom_shader_speed")]
450    pub cursor_shader_animation_speed: f32,
451
452    /// Cursor color for shader effects [R, G, B] (0-255)
453    /// This color is passed to the shader via iCursorShaderColor uniform
454    #[serde(default = "crate::defaults::cursor_shader_color")]
455    pub cursor_shader_color: [u8; 3],
456
457    /// Duration of cursor trail effect in seconds
458    /// Passed to shader via iCursorTrailDuration uniform
459    #[serde(default = "crate::defaults::cursor_trail_duration")]
460    pub cursor_shader_trail_duration: f32,
461
462    /// Radius of cursor glow effect in pixels
463    /// Passed to shader via iCursorGlowRadius uniform
464    #[serde(default = "crate::defaults::cursor_glow_radius")]
465    pub cursor_shader_glow_radius: f32,
466
467    /// Intensity of cursor glow effect (0.0 = none, 1.0 = full)
468    /// Passed to shader via iCursorGlowIntensity uniform
469    #[serde(default = "crate::defaults::cursor_glow_intensity")]
470    pub cursor_shader_glow_intensity: f32,
471
472    /// Hide the default cursor when cursor shader is enabled
473    /// When true and cursor_shader_enabled is true, the normal cursor is not drawn
474    /// This allows cursor shaders to fully replace the cursor rendering
475    #[serde(default = "crate::defaults::bool_false")]
476    pub cursor_shader_hides_cursor: bool,
477
478    /// Disable cursor shader while in alt screen (vim, less, htop)
479    /// Keeps current behavior by default for TUI compatibility
480    #[serde(default = "crate::defaults::cursor_shader_disable_in_alt_screen")]
481    pub cursor_shader_disable_in_alt_screen: bool,
482
483    // ========================================================================
484    // Keyboard Input
485    // ========================================================================
486    /// Left Option key (macOS) / Left Alt key (Linux/Windows) behavior
487    /// - normal: Sends special characters (default macOS behavior)
488    /// - meta: Sets the high bit (8th bit) on the character
489    /// - esc: Sends Escape prefix before the character (most compatible for emacs/vim)
490    #[serde(default)]
491    pub left_option_key_mode: OptionKeyMode,
492
493    /// Right Option key (macOS) / Right Alt key (Linux/Windows) behavior
494    /// Can be configured independently from left Option key
495    /// - normal: Sends special characters (default macOS behavior)
496    /// - meta: Sets the high bit (8th bit) on the character
497    /// - esc: Sends Escape prefix before the character (most compatible for emacs/vim)
498    #[serde(default)]
499    pub right_option_key_mode: OptionKeyMode,
500
501    /// Modifier key remapping configuration
502    /// Allows remapping modifier keys to different functions (e.g., swap Ctrl and Caps Lock)
503    #[serde(default)]
504    pub modifier_remapping: ModifierRemapping,
505
506    /// Use physical key positions for keybindings instead of logical characters
507    /// When enabled, keybindings work based on key position (scan code) rather than
508    /// the character produced, making shortcuts consistent across keyboard layouts.
509    /// For example, Ctrl+Z will always be the bottom-left key regardless of QWERTY/AZERTY/Dvorak.
510    #[serde(default = "crate::defaults::bool_false")]
511    pub use_physical_keys: bool,
512
513    // ========================================================================
514    // Selection & Clipboard
515    // ========================================================================
516    /// Automatically copy selected text to clipboard
517    #[serde(default = "crate::defaults::bool_true")]
518    pub auto_copy_selection: bool,
519
520    /// Include trailing newline when copying lines
521    /// Note: Inverted logic from old strip_trailing_newline_on_copy
522    #[serde(
523        default = "crate::defaults::bool_false",
524        alias = "strip_trailing_newline_on_copy"
525    )]
526    pub copy_trailing_newline: bool,
527
528    /// Paste on middle mouse button click
529    #[serde(default = "crate::defaults::bool_true")]
530    pub middle_click_paste: bool,
531
532    /// Delay in milliseconds between pasted lines (0 = no delay)
533    /// Useful for slow terminals or remote connections that can't handle rapid paste
534    #[serde(default = "crate::defaults::paste_delay_ms")]
535    pub paste_delay_ms: u64,
536
537    /// Quote style for dropped file paths
538    /// - single_quotes: Wrap in single quotes (safest for most shells)
539    /// - double_quotes: Wrap in double quotes
540    /// - backslash: Escape special characters with backslashes
541    /// - none: Insert path as-is (not recommended)
542    #[serde(default)]
543    pub dropped_file_quote_style: DroppedFileQuoteStyle,
544
545    // ========================================================================
546    // Mouse Behavior
547    // ========================================================================
548    /// Mouse wheel scroll speed multiplier
549    #[serde(default = "crate::defaults::scroll_speed")]
550    pub mouse_scroll_speed: f32,
551
552    /// Double-click timing threshold in milliseconds
553    #[serde(default = "crate::defaults::double_click_threshold")]
554    pub mouse_double_click_threshold: u64,
555
556    /// Triple-click timing threshold in milliseconds (typically same as double-click)
557    #[serde(default = "crate::defaults::triple_click_threshold")]
558    pub mouse_triple_click_threshold: u64,
559
560    /// Option+Click (macOS) / Alt+Click (Linux/Windows) moves cursor to clicked position
561    /// Sends cursor movement escape sequences to position text cursor at click location
562    /// Useful for quick cursor positioning in shells and editors
563    #[serde(default = "crate::defaults::bool_true")]
564    pub option_click_moves_cursor: bool,
565
566    /// Focus window automatically when mouse enters (without requiring a click)
567    /// This is an accessibility feature that some users prefer
568    #[serde(default = "crate::defaults::bool_false")]
569    pub focus_follows_mouse: bool,
570
571    /// Report horizontal scroll events to terminal applications when mouse reporting is enabled
572    /// Horizontal scroll uses button codes 6 (left) and 7 (right) in the mouse protocol
573    #[serde(default = "crate::defaults::bool_true")]
574    pub report_horizontal_scroll: bool,
575
576    // ========================================================================
577    // Word Selection
578    // ========================================================================
579    /// Characters considered part of a word for double-click selection (in addition to alphanumeric)
580    /// Default: "/-+\\~_." (matches iTerm2)
581    /// Example: If you want to select entire paths, add "/" to include path separators
582    #[serde(default = "crate::defaults::word_characters")]
583    pub word_characters: String,
584
585    /// Enable smart selection rules for pattern-based double-click selection
586    /// When enabled, double-click will try to match patterns like URLs, emails, paths
587    /// before falling back to word boundary selection
588    #[serde(default = "crate::defaults::smart_selection_enabled")]
589    pub smart_selection_enabled: bool,
590
591    /// Smart selection rules for pattern-based double-click selection
592    /// Rules are evaluated by precision (highest first). If a pattern matches
593    /// at the cursor position, that text is selected instead of using word boundaries.
594    #[serde(default = "crate::types::default_smart_selection_rules")]
595    pub smart_selection_rules: Vec<SmartSelectionRule>,
596
597    // ========================================================================
598    // Copy Mode (vi-style keyboard-driven selection)
599    // ========================================================================
600    /// Enable copy mode (vi-style keyboard-driven text selection and navigation).
601    /// When enabled, users can enter copy mode via the `toggle_copy_mode` keybinding
602    /// action to navigate the terminal buffer with vi keys and yank text.
603    #[serde(default = "crate::defaults::bool_true")]
604    pub copy_mode_enabled: bool,
605
606    /// Automatically exit copy mode after yanking (copying) selected text.
607    /// When true (default), pressing `y` in visual mode copies text and exits copy mode.
608    /// When false, copy mode stays active after yanking so you can continue selecting.
609    #[serde(default = "crate::defaults::bool_true")]
610    pub copy_mode_auto_exit_on_yank: bool,
611
612    /// Show a status bar at the bottom of the terminal when copy mode is active.
613    /// The status bar displays the current mode (COPY/VISUAL/V-LINE/V-BLOCK/SEARCH)
614    /// and cursor position information.
615    #[serde(default = "crate::defaults::bool_true")]
616    pub copy_mode_show_status: bool,
617
618    // ========================================================================
619    // Scrollback & Cursor
620    // ========================================================================
621    /// Maximum number of lines to keep in scrollback buffer
622    #[serde(default = "crate::defaults::scrollback", alias = "scrollback_size")]
623    pub scrollback_lines: usize,
624
625    // ========================================================================
626    // Unicode Width Settings
627    // ========================================================================
628    /// Unicode version for character width calculations
629    /// Different versions have different width tables, particularly for emoji.
630    /// Options: unicode_9, unicode_10, ..., unicode_16, auto (default)
631    #[serde(default = "crate::defaults::unicode_version")]
632    pub unicode_version: par_term_emu_core_rust::UnicodeVersion,
633
634    /// Treatment of East Asian Ambiguous width characters
635    /// - narrow: 1 cell width (Western default)
636    /// - wide: 2 cell width (CJK default)
637    #[serde(default = "crate::defaults::ambiguous_width")]
638    pub ambiguous_width: par_term_emu_core_rust::AmbiguousWidth,
639
640    /// Unicode normalization form for text processing
641    /// Controls how Unicode text is normalized before being stored in terminal cells.
642    /// - NFC: Canonical composition (default, most compatible)
643    /// - NFD: Canonical decomposition (macOS HFS+ style)
644    /// - NFKC: Compatibility composition (resolves ligatures like fi → fi)
645    /// - NFKD: Compatibility decomposition
646    /// - none: No normalization
647    #[serde(default = "crate::defaults::normalization_form")]
648    pub normalization_form: par_term_emu_core_rust::NormalizationForm,
649
650    /// Enable cursor blinking
651    #[serde(default = "crate::defaults::bool_false")]
652    pub cursor_blink: bool,
653
654    /// Cursor blink interval in milliseconds
655    #[serde(default = "crate::defaults::cursor_blink_interval")]
656    pub cursor_blink_interval: u64,
657
658    /// Cursor style (block, beam, underline)
659    #[serde(default)]
660    pub cursor_style: CursorStyle,
661
662    /// Cursor color [R, G, B] (0-255)
663    #[serde(default = "crate::defaults::cursor_color")]
664    pub cursor_color: [u8; 3],
665
666    /// Color of text under block cursor [R, G, B] (0-255)
667    /// If not set (None), uses automatic contrast color
668    /// Only affects block cursor style (beam and underline don't obscure text)
669    #[serde(default)]
670    pub cursor_text_color: Option<[u8; 3]>,
671
672    /// Lock cursor visibility - prevent applications from hiding the cursor
673    /// When true, the cursor remains visible regardless of DECTCEM escape sequences
674    #[serde(default = "crate::defaults::bool_false")]
675    pub lock_cursor_visibility: bool,
676
677    /// Lock cursor style - prevent applications from changing the cursor style
678    /// When true, the cursor style from config is always used, ignoring DECSCUSR escape sequences
679    #[serde(default = "crate::defaults::bool_false")]
680    pub lock_cursor_style: bool,
681
682    /// Lock cursor blink - prevent applications from enabling cursor blink
683    /// When true and cursor_blink is false, applications cannot enable blinking cursor
684    #[serde(default = "crate::defaults::bool_false")]
685    pub lock_cursor_blink: bool,
686
687    // ========================================================================
688    // Cursor Enhancements (iTerm2-style features)
689    // ========================================================================
690    /// Enable horizontal guide line at cursor row for better tracking in wide terminals
691    #[serde(default = "crate::defaults::bool_false")]
692    pub cursor_guide_enabled: bool,
693
694    /// Cursor guide color [R, G, B, A] (0-255), subtle highlight spanning full terminal width
695    #[serde(default = "crate::defaults::cursor_guide_color")]
696    pub cursor_guide_color: [u8; 4],
697
698    /// Enable drop shadow behind cursor for better visibility against varying backgrounds
699    #[serde(default = "crate::defaults::bool_false")]
700    pub cursor_shadow_enabled: bool,
701
702    /// Cursor shadow color [R, G, B, A] (0-255)
703    #[serde(default = "crate::defaults::cursor_shadow_color")]
704    pub cursor_shadow_color: [u8; 4],
705
706    /// Cursor shadow offset in pixels [x, y]
707    #[serde(default = "crate::defaults::cursor_shadow_offset")]
708    pub cursor_shadow_offset: [f32; 2],
709
710    /// Cursor shadow blur radius in pixels
711    #[serde(default = "crate::defaults::cursor_shadow_blur")]
712    pub cursor_shadow_blur: f32,
713
714    /// Cursor boost (glow) intensity (0.0 = off, 1.0 = maximum boost)
715    /// Adds a glow/highlight effect around the cursor for visibility
716    #[serde(default = "crate::defaults::cursor_boost")]
717    pub cursor_boost: f32,
718
719    /// Cursor boost glow color [R, G, B] (0-255)
720    #[serde(default = "crate::defaults::cursor_boost_color")]
721    pub cursor_boost_color: [u8; 3],
722
723    /// Cursor appearance when window is unfocused
724    /// - hollow: Show outline-only block cursor (default, standard terminal behavior)
725    /// - same: Keep same cursor style as when focused
726    /// - hidden: Hide cursor completely when unfocused
727    #[serde(default)]
728    pub unfocused_cursor_style: UnfocusedCursorStyle,
729
730    // ========================================================================
731    // Scrollbar
732    // ========================================================================
733    /// Auto-hide scrollbar after inactivity (milliseconds, 0 = never hide)
734    #[serde(default = "crate::defaults::scrollbar_autohide_delay")]
735    pub scrollbar_autohide_delay: u64,
736
737    // ========================================================================
738    // Theme & Colors
739    // ========================================================================
740    /// Color theme name to use for terminal colors
741    #[serde(default = "crate::defaults::theme")]
742    pub theme: String,
743
744    /// Automatically switch theme based on system light/dark mode
745    #[serde(default)]
746    pub auto_dark_mode: bool,
747
748    /// Theme to use when system is in light mode (used when auto_dark_mode is true)
749    #[serde(default = "crate::defaults::light_theme")]
750    pub light_theme: String,
751
752    /// Theme to use when system is in dark mode (used when auto_dark_mode is true)
753    #[serde(default = "crate::defaults::dark_theme")]
754    pub dark_theme: String,
755
756    // ========================================================================
757    // Screenshot
758    // ========================================================================
759    /// File format for screenshots (png, jpeg, svg, html)
760    #[serde(default = "crate::defaults::screenshot_format")]
761    pub screenshot_format: String,
762
763    // ========================================================================
764    // Shell Behavior
765    // ========================================================================
766    /// Action to take when the shell process exits
767    /// Supports: close, keep, restart_immediately, restart_with_prompt, restart_after_delay
768    /// For backward compatibility, also accepts boolean values (true=close, false=keep)
769    #[serde(
770        default,
771        deserialize_with = "deserialize_shell_exit_action",
772        alias = "exit_on_shell_exit",
773        alias = "close_on_shell_exit"
774    )]
775    pub shell_exit_action: ShellExitAction,
776
777    /// Custom shell command (defaults to system shell if not specified)
778    #[serde(default)]
779    pub custom_shell: Option<String>,
780
781    /// Arguments to pass to the shell
782    #[serde(default)]
783    pub shell_args: Option<Vec<String>>,
784
785    /// Working directory for the shell (legacy, use startup_directory_mode instead)
786    /// When set, overrides startup_directory_mode for backward compatibility
787    #[serde(default)]
788    pub working_directory: Option<String>,
789
790    /// Startup directory mode: controls where new sessions start
791    /// - home: Start in user's home directory (default)
792    /// - previous: Remember and restore last working directory from previous session
793    /// - custom: Start in the directory specified by startup_directory
794    #[serde(default)]
795    pub startup_directory_mode: StartupDirectoryMode,
796
797    /// Custom startup directory (used when startup_directory_mode is "custom")
798    /// Supports ~ for home directory expansion
799    #[serde(default)]
800    pub startup_directory: Option<String>,
801
802    /// Last working directory from previous session (auto-managed)
803    /// Used when startup_directory_mode is "previous"
804    #[serde(default)]
805    pub last_working_directory: Option<String>,
806
807    /// Environment variables to set for the shell
808    #[serde(default)]
809    pub shell_env: Option<std::collections::HashMap<String, String>>,
810
811    /// Whether to spawn the shell as a login shell (passes -l flag)
812    /// This is important on macOS to properly initialize PATH from Homebrew, /etc/paths.d, etc.
813    /// Default: true
814    #[serde(default = "crate::defaults::login_shell")]
815    pub login_shell: bool,
816
817    /// Text to send automatically when a terminal session starts
818    /// Supports escape sequences: \n (newline), \r (carriage return), \t (tab), \xHH (hex), \e (ESC)
819    #[serde(default = "crate::defaults::initial_text")]
820    pub initial_text: String,
821
822    /// Delay in milliseconds before sending the initial text (to allow shell to be ready)
823    #[serde(default = "crate::defaults::initial_text_delay_ms")]
824    pub initial_text_delay_ms: u64,
825
826    /// Whether to append a newline after sending the initial text
827    #[serde(default = "crate::defaults::initial_text_send_newline")]
828    pub initial_text_send_newline: bool,
829
830    /// Answerback string sent in response to ENQ (0x05) control character
831    /// This is a legacy terminal feature used for terminal identification.
832    /// Default: empty (disabled) for security
833    /// Common values: "par-term", "vt100", or custom identification
834    /// Security note: Setting this may expose terminal identification to applications
835    #[serde(default = "crate::defaults::answerback_string")]
836    pub answerback_string: String,
837
838    /// Show confirmation dialog before quitting the application
839    /// When enabled, closing the window will show a confirmation dialog
840    /// if there are any open terminal sessions.
841    /// Default: false (close immediately without confirmation)
842    #[serde(default = "crate::defaults::bool_false")]
843    pub prompt_on_quit: bool,
844
845    /// Show confirmation dialog before closing a tab with running jobs
846    /// When enabled, closing a tab that has a running command will show a confirmation dialog.
847    /// Default: false (close immediately without confirmation)
848    #[serde(default = "crate::defaults::bool_false")]
849    pub confirm_close_running_jobs: bool,
850
851    /// List of job/process names to ignore when checking for running jobs
852    /// These jobs will not trigger a close confirmation dialog.
853    /// Common examples: "bash", "zsh", "fish", "cat", "less", "man", "sleep"
854    /// Default: common shell names that shouldn't block tab close
855    #[serde(default = "crate::defaults::jobs_to_ignore")]
856    pub jobs_to_ignore: Vec<String>,
857
858    // ========================================================================
859    // Semantic History
860    // ========================================================================
861    /// Enable semantic history (file path detection and opening)
862    /// When enabled, Cmd/Ctrl+Click on detected file paths opens them in the editor.
863    #[serde(default = "crate::defaults::bool_true")]
864    pub semantic_history_enabled: bool,
865
866    /// Editor selection mode for semantic history
867    ///
868    /// - `custom` - Use the editor command specified in `semantic_history_editor`
869    /// - `environment_variable` - Use `$EDITOR` or `$VISUAL` environment variable (default)
870    /// - `system_default` - Use system default application for each file type
871    #[serde(default)]
872    pub semantic_history_editor_mode: SemanticHistoryEditorMode,
873
874    /// Editor command for semantic history (when mode is `custom`).
875    ///
876    /// Placeholders: `{file}` = file path, `{line}` = line number (if available)
877    ///
878    /// Examples:
879    /// - `code -g {file}:{line}` (VS Code with line number)
880    /// - `subl {file}:{line}` (Sublime Text)
881    /// - `vim +{line} {file}` (Vim)
882    /// - `emacs +{line} {file}` (Emacs)
883    #[serde(default = "crate::defaults::semantic_history_editor")]
884    pub semantic_history_editor: String,
885
886    /// Color for highlighted links (URLs and file paths) [R, G, B] (0-255)
887    #[serde(default = "crate::defaults::link_highlight_color")]
888    pub link_highlight_color: [u8; 3],
889
890    /// Underline highlighted links (URLs and file paths)
891    #[serde(default = "crate::defaults::bool_true")]
892    pub link_highlight_underline: bool,
893
894    /// Style for link highlight underlines (solid or stipple)
895    #[serde(default)]
896    pub link_underline_style: crate::types::LinkUnderlineStyle,
897
898    /// Custom command to open URLs. When set, used instead of system default browser.
899    ///
900    /// Use `{url}` as placeholder for the URL.
901    ///
902    /// Examples:
903    /// - `firefox {url}` (open in Firefox)
904    /// - `open -a Safari {url}` (macOS: open in Safari)
905    /// - `chromium-browser {url}` (Linux: open in Chromium)
906    ///
907    /// When empty or unset, uses the system default browser.
908    #[serde(default)]
909    pub link_handler_command: String,
910
911    // ========================================================================
912    // Scrollbar (GUI-specific)
913    // ========================================================================
914    /// Scrollbar position (left or right)
915    #[serde(default = "crate::defaults::scrollbar_position")]
916    pub scrollbar_position: String,
917
918    /// Scrollbar width in pixels
919    #[serde(default = "crate::defaults::scrollbar_width")]
920    pub scrollbar_width: f32,
921
922    /// Scrollbar thumb color (RGBA: [r, g, b, a] where each is 0.0-1.0)
923    #[serde(default = "crate::defaults::scrollbar_thumb_color")]
924    pub scrollbar_thumb_color: [f32; 4],
925
926    /// Scrollbar track color (RGBA: [r, g, b, a] where each is 0.0-1.0)
927    #[serde(default = "crate::defaults::scrollbar_track_color")]
928    pub scrollbar_track_color: [f32; 4],
929
930    /// Show command markers on the scrollbar (requires shell integration)
931    #[serde(default = "crate::defaults::bool_true")]
932    pub scrollbar_command_marks: bool,
933
934    /// Show tooltips when hovering over scrollbar command markers
935    #[serde(default = "crate::defaults::bool_false")]
936    pub scrollbar_mark_tooltips: bool,
937
938    // ========================================================================
939    // Command Separator Lines
940    // ========================================================================
941    /// Show horizontal separator lines between commands (requires shell integration)
942    #[serde(default = "crate::defaults::bool_false")]
943    pub command_separator_enabled: bool,
944
945    /// Thickness of command separator lines in pixels
946    #[serde(default = "crate::defaults::command_separator_thickness")]
947    pub command_separator_thickness: f32,
948
949    /// Opacity of command separator lines (0.0-1.0)
950    #[serde(default = "crate::defaults::command_separator_opacity")]
951    pub command_separator_opacity: f32,
952
953    /// Color separator lines by exit code (green=success, red=failure, gray=unknown)
954    #[serde(default = "crate::defaults::bool_true")]
955    pub command_separator_exit_color: bool,
956
957    /// Custom color for separator lines when exit_color is disabled [R, G, B]
958    #[serde(default = "crate::defaults::command_separator_color")]
959    pub command_separator_color: [u8; 3],
960
961    // ========================================================================
962    // Clipboard Sync Limits
963    // ========================================================================
964    /// Maximum clipboard sync events retained for diagnostics
965    #[serde(
966        default = "crate::defaults::clipboard_max_sync_events",
967        alias = "max_clipboard_sync_events"
968    )]
969    pub clipboard_max_sync_events: usize,
970
971    /// Maximum bytes stored per clipboard sync event
972    #[serde(
973        default = "crate::defaults::clipboard_max_event_bytes",
974        alias = "max_clipboard_event_bytes"
975    )]
976    pub clipboard_max_event_bytes: usize,
977
978    // ========================================================================
979    // Command History
980    // ========================================================================
981    /// Maximum number of commands to persist in fuzzy search history
982    #[serde(default = "crate::defaults::command_history_max_entries")]
983    pub command_history_max_entries: usize,
984
985    // ========================================================================
986    // Notifications
987    // ========================================================================
988    /// Forward BEL events to desktop notification centers
989    #[serde(default = "crate::defaults::bool_false", alias = "bell_desktop")]
990    pub notification_bell_desktop: bool,
991
992    /// Volume (0-100) for backend bell sound alerts (0 disables)
993    #[serde(default = "crate::defaults::bell_sound", alias = "bell_sound")]
994    pub notification_bell_sound: u8,
995
996    /// Enable backend visual bell overlay
997    #[serde(default = "crate::defaults::bool_true", alias = "bell_visual")]
998    pub notification_bell_visual: bool,
999
1000    /// Enable notifications when activity resumes after inactivity
1001    #[serde(
1002        default = "crate::defaults::bool_false",
1003        alias = "activity_notifications"
1004    )]
1005    pub notification_activity_enabled: bool,
1006
1007    /// Seconds of inactivity required before an activity alert fires
1008    #[serde(
1009        default = "crate::defaults::activity_threshold",
1010        alias = "activity_threshold"
1011    )]
1012    pub notification_activity_threshold: u64,
1013
1014    /// Enable anti-idle keep-alive (sends code after idle period)
1015    #[serde(default = "crate::defaults::bool_false")]
1016    pub anti_idle_enabled: bool,
1017
1018    /// Seconds of inactivity before sending keep-alive code
1019    #[serde(default = "crate::defaults::anti_idle_seconds")]
1020    pub anti_idle_seconds: u64,
1021
1022    /// ASCII code to send as keep-alive (e.g., 0 = NUL, 27 = ESC)
1023    #[serde(default = "crate::defaults::anti_idle_code")]
1024    pub anti_idle_code: u8,
1025
1026    /// Enable notifications after prolonged silence
1027    #[serde(
1028        default = "crate::defaults::bool_false",
1029        alias = "silence_notifications"
1030    )]
1031    pub notification_silence_enabled: bool,
1032
1033    /// Seconds of silence before a silence alert fires
1034    #[serde(
1035        default = "crate::defaults::silence_threshold",
1036        alias = "silence_threshold"
1037    )]
1038    pub notification_silence_threshold: u64,
1039
1040    /// Enable notification when a shell/session exits
1041    #[serde(default = "crate::defaults::bool_false", alias = "session_ended")]
1042    pub notification_session_ended: bool,
1043
1044    /// Suppress desktop notifications when the terminal window is focused
1045    #[serde(default = "crate::defaults::bool_true")]
1046    pub suppress_notifications_when_focused: bool,
1047
1048    /// Maximum number of OSC 9/777 notification entries retained by backend
1049    #[serde(
1050        default = "crate::defaults::notification_max_buffer",
1051        alias = "max_notifications"
1052    )]
1053    pub notification_max_buffer: usize,
1054
1055    /// Alert sound configuration per event type
1056    /// Maps AlertEvent variants to their sound settings
1057    #[serde(default)]
1058    pub alert_sounds: HashMap<AlertEvent, AlertSoundConfig>,
1059
1060    // ========================================================================
1061    // SSH Settings
1062    // ========================================================================
1063    /// Enable mDNS/Bonjour discovery for SSH hosts
1064    #[serde(default = "crate::defaults::bool_false")]
1065    pub enable_mdns_discovery: bool,
1066
1067    /// mDNS scan timeout in seconds
1068    #[serde(default = "crate::defaults::mdns_timeout")]
1069    pub mdns_scan_timeout_secs: u32,
1070
1071    /// Enable automatic profile switching based on SSH hostname
1072    #[serde(default = "crate::defaults::bool_true")]
1073    pub ssh_auto_profile_switch: bool,
1074
1075    /// Revert profile when SSH session disconnects
1076    #[serde(default = "crate::defaults::bool_true")]
1077    pub ssh_revert_profile_on_disconnect: bool,
1078
1079    // ========================================================================
1080    // Tab Settings
1081    // ========================================================================
1082    /// Tab visual style preset (dark, light, compact, minimal, high_contrast, automatic)
1083    /// Applies cosmetic color/size/spacing presets to the tab bar
1084    #[serde(default)]
1085    pub tab_style: TabStyle,
1086
1087    /// Tab style to use when system is in light mode (used when tab_style is Automatic)
1088    #[serde(default = "crate::defaults::light_tab_style")]
1089    pub light_tab_style: TabStyle,
1090
1091    /// Tab style to use when system is in dark mode (used when tab_style is Automatic)
1092    #[serde(default = "crate::defaults::dark_tab_style")]
1093    pub dark_tab_style: TabStyle,
1094
1095    /// Tab bar visibility mode (always, when_multiple, never)
1096    #[serde(default)]
1097    pub tab_bar_mode: TabBarMode,
1098
1099    /// Tab bar height in pixels
1100    #[serde(default = "crate::defaults::tab_bar_height")]
1101    pub tab_bar_height: f32,
1102
1103    /// Tab bar position (top, bottom, left)
1104    #[serde(default)]
1105    pub tab_bar_position: TabBarPosition,
1106
1107    /// Tab bar width in pixels (used when tab_bar_position is Left)
1108    #[serde(default = "crate::defaults::tab_bar_width")]
1109    pub tab_bar_width: f32,
1110
1111    /// Show close button on tabs
1112    #[serde(default = "crate::defaults::bool_true")]
1113    pub tab_show_close_button: bool,
1114
1115    /// Show tab index numbers (for Cmd+1-9)
1116    #[serde(default = "crate::defaults::bool_false")]
1117    pub tab_show_index: bool,
1118
1119    /// New tab inherits working directory from active tab
1120    #[serde(default = "crate::defaults::bool_true")]
1121    pub tab_inherit_cwd: bool,
1122
1123    /// Maximum tabs per window (0 = unlimited)
1124    #[serde(default = "crate::defaults::zero")]
1125    pub max_tabs: usize,
1126
1127    /// Show the profile drawer toggle button on the right edge of the terminal
1128    /// When disabled, the profile drawer can still be opened via keyboard shortcut
1129    #[serde(default = "crate::defaults::bool_false")]
1130    pub show_profile_drawer_button: bool,
1131
1132    /// When true, the new-tab keyboard shortcut (Cmd+T / Ctrl+Shift+T) shows the
1133    /// profile selection dropdown instead of immediately opening a default tab
1134    #[serde(default = "crate::defaults::bool_false")]
1135    pub new_tab_shortcut_shows_profiles: bool,
1136
1137    // ========================================================================
1138    // Tab Bar Colors
1139    // ========================================================================
1140    /// Tab bar background color [R, G, B] (0-255)
1141    #[serde(default = "crate::defaults::tab_bar_background")]
1142    pub tab_bar_background: [u8; 3],
1143
1144    /// Active tab background color [R, G, B] (0-255)
1145    #[serde(default = "crate::defaults::tab_active_background")]
1146    pub tab_active_background: [u8; 3],
1147
1148    /// Inactive tab background color [R, G, B] (0-255)
1149    #[serde(default = "crate::defaults::tab_inactive_background")]
1150    pub tab_inactive_background: [u8; 3],
1151
1152    /// Hovered tab background color [R, G, B] (0-255)
1153    #[serde(default = "crate::defaults::tab_hover_background")]
1154    pub tab_hover_background: [u8; 3],
1155
1156    /// Active tab text color [R, G, B] (0-255)
1157    #[serde(default = "crate::defaults::tab_active_text")]
1158    pub tab_active_text: [u8; 3],
1159
1160    /// Inactive tab text color [R, G, B] (0-255)
1161    #[serde(default = "crate::defaults::tab_inactive_text")]
1162    pub tab_inactive_text: [u8; 3],
1163
1164    /// Active tab indicator/underline color [R, G, B] (0-255)
1165    #[serde(default = "crate::defaults::tab_active_indicator")]
1166    pub tab_active_indicator: [u8; 3],
1167
1168    /// Activity indicator dot color [R, G, B] (0-255)
1169    #[serde(default = "crate::defaults::tab_activity_indicator")]
1170    pub tab_activity_indicator: [u8; 3],
1171
1172    /// Bell indicator color [R, G, B] (0-255)
1173    #[serde(default = "crate::defaults::tab_bell_indicator")]
1174    pub tab_bell_indicator: [u8; 3],
1175
1176    /// Close button color [R, G, B] (0-255)
1177    #[serde(default = "crate::defaults::tab_close_button")]
1178    pub tab_close_button: [u8; 3],
1179
1180    /// Close button hover color [R, G, B] (0-255)
1181    #[serde(default = "crate::defaults::tab_close_button_hover")]
1182    pub tab_close_button_hover: [u8; 3],
1183
1184    /// Enable visual dimming of inactive tabs
1185    /// When true, inactive tabs are rendered with reduced opacity
1186    #[serde(default = "crate::defaults::bool_true")]
1187    pub dim_inactive_tabs: bool,
1188
1189    /// Opacity level for inactive tabs (0.0-1.0)
1190    /// Only used when dim_inactive_tabs is true
1191    /// Lower values make inactive tabs more transparent/dimmed
1192    #[serde(default = "crate::defaults::inactive_tab_opacity")]
1193    pub inactive_tab_opacity: f32,
1194
1195    /// Minimum tab width in pixels before horizontal scrolling is enabled
1196    /// When tabs cannot fit at this width, scroll buttons appear
1197    #[serde(default = "crate::defaults::tab_min_width")]
1198    pub tab_min_width: f32,
1199
1200    /// Stretch tabs to fill the available tab bar width evenly (iTerm2 style)
1201    /// When false, tabs keep their minimum width and excess space is left unused
1202    #[serde(default = "crate::defaults::tab_stretch_to_fill")]
1203    pub tab_stretch_to_fill: bool,
1204
1205    /// Render tab titles as limited HTML (bold/italic/underline/color spans)
1206    /// When false, titles are rendered as plain text
1207    #[serde(default = "crate::defaults::tab_html_titles")]
1208    pub tab_html_titles: bool,
1209
1210    /// Tab border color [R, G, B] (0-255)
1211    /// A thin border around each tab to help distinguish them
1212    #[serde(default = "crate::defaults::tab_border_color")]
1213    pub tab_border_color: [u8; 3],
1214
1215    /// Tab border width in pixels (0 = no border)
1216    #[serde(default = "crate::defaults::tab_border_width")]
1217    pub tab_border_width: f32,
1218
1219    /// Render inactive tabs as outline only (no fill)
1220    /// When true, inactive tabs show only a border stroke with no background fill.
1221    /// Hovered inactive tabs brighten the outline instead of filling.
1222    #[serde(default = "crate::defaults::bool_false")]
1223    pub tab_inactive_outline_only: bool,
1224
1225    // ========================================================================
1226    // Split Pane Settings
1227    // ========================================================================
1228    /// Width of dividers between panes in pixels (visual width)
1229    #[serde(default = "crate::defaults::pane_divider_width")]
1230    pub pane_divider_width: Option<f32>,
1231
1232    /// Width of the drag hit area for resizing panes (should be >= divider width)
1233    /// A larger hit area makes it easier to grab the divider for resizing
1234    #[serde(default = "crate::defaults::pane_divider_hit_width")]
1235    pub pane_divider_hit_width: f32,
1236
1237    /// Padding inside panes in pixels (space between content and border/divider)
1238    #[serde(default = "crate::defaults::pane_padding")]
1239    pub pane_padding: f32,
1240
1241    /// Minimum pane size in cells (columns for horizontal splits, rows for vertical)
1242    /// Prevents panes from being resized too small to be useful
1243    #[serde(default = "crate::defaults::pane_min_size")]
1244    pub pane_min_size: usize,
1245
1246    /// Pane background opacity (0.0 = fully transparent, 1.0 = fully opaque)
1247    /// Lower values allow background image/shader to show through pane backgrounds
1248    #[serde(default = "crate::defaults::pane_background_opacity")]
1249    pub pane_background_opacity: f32,
1250
1251    /// Pane divider color [R, G, B] (0-255)
1252    #[serde(default = "crate::defaults::pane_divider_color")]
1253    pub pane_divider_color: [u8; 3],
1254
1255    /// Pane divider hover color [R, G, B] (0-255) - shown when mouse hovers over divider
1256    #[serde(default = "crate::defaults::pane_divider_hover_color")]
1257    pub pane_divider_hover_color: [u8; 3],
1258
1259    /// Enable visual dimming of inactive panes
1260    #[serde(default = "crate::defaults::bool_false")]
1261    pub dim_inactive_panes: bool,
1262
1263    /// Opacity level for inactive panes (0.0-1.0)
1264    #[serde(default = "crate::defaults::inactive_pane_opacity")]
1265    pub inactive_pane_opacity: f32,
1266
1267    /// Show title bar on each pane
1268    #[serde(default = "crate::defaults::bool_false")]
1269    pub show_pane_titles: bool,
1270
1271    /// Height of pane title bars in pixels
1272    #[serde(default = "crate::defaults::pane_title_height")]
1273    pub pane_title_height: f32,
1274
1275    /// Position of pane title bars (top or bottom)
1276    #[serde(default)]
1277    pub pane_title_position: PaneTitlePosition,
1278
1279    /// Pane title text color [R, G, B] (0-255)
1280    #[serde(default = "crate::defaults::pane_title_color")]
1281    pub pane_title_color: [u8; 3],
1282
1283    /// Pane title background color [R, G, B] (0-255)
1284    #[serde(default = "crate::defaults::pane_title_bg_color")]
1285    pub pane_title_bg_color: [u8; 3],
1286
1287    /// Pane title font family (empty string = use terminal font)
1288    #[serde(default)]
1289    pub pane_title_font: String,
1290
1291    /// Style of dividers between panes (solid, double, dashed, shadow)
1292    #[serde(default)]
1293    pub pane_divider_style: DividerStyle,
1294
1295    /// Maximum panes per tab (0 = unlimited)
1296    #[serde(default = "crate::defaults::max_panes")]
1297    pub max_panes: usize,
1298
1299    /// Show visual indicator (border) around focused pane
1300    #[serde(default = "crate::defaults::bool_true")]
1301    pub pane_focus_indicator: bool,
1302
1303    /// Color of the focused pane indicator [R, G, B] (0-255)
1304    #[serde(default = "crate::defaults::pane_focus_color")]
1305    pub pane_focus_color: [u8; 3],
1306
1307    /// Width of the focused pane indicator border in pixels
1308    #[serde(default = "crate::defaults::pane_focus_width")]
1309    pub pane_focus_width: f32,
1310
1311    // ========================================================================
1312    // tmux Integration
1313    // ========================================================================
1314    /// Enable tmux control mode integration
1315    #[serde(default = "crate::defaults::bool_false")]
1316    pub tmux_enabled: bool,
1317
1318    /// Path to tmux executable (default: "tmux" - uses PATH)
1319    #[serde(default = "crate::defaults::tmux_path")]
1320    pub tmux_path: String,
1321
1322    /// Default session name when creating new tmux sessions
1323    #[serde(default = "crate::defaults::tmux_default_session")]
1324    pub tmux_default_session: Option<String>,
1325
1326    /// Auto-attach to existing tmux session on startup
1327    #[serde(default = "crate::defaults::bool_false")]
1328    pub tmux_auto_attach: bool,
1329
1330    /// Session name to auto-attach to (if tmux_auto_attach is true)
1331    #[serde(default = "crate::defaults::tmux_auto_attach_session")]
1332    pub tmux_auto_attach_session: Option<String>,
1333
1334    /// Sync clipboard with tmux paste buffer
1335    /// When copying in par-term, also update tmux's paste buffer via set-buffer
1336    #[serde(default = "crate::defaults::bool_true")]
1337    pub tmux_clipboard_sync: bool,
1338
1339    /// Profile to switch to when connected to tmux
1340    /// When profiles feature is implemented, this will automatically
1341    /// switch to the specified profile when entering tmux mode
1342    #[serde(default)]
1343    pub tmux_profile: Option<String>,
1344
1345    /// Show tmux status bar in par-term UI
1346    /// When connected to tmux, display the status bar at the bottom of the terminal
1347    #[serde(default = "crate::defaults::bool_false")]
1348    pub tmux_show_status_bar: bool,
1349
1350    /// Tmux status bar refresh interval in milliseconds
1351    /// How often to poll tmux for updated status bar content.
1352    /// Lower values mean more frequent updates but slightly more CPU usage.
1353    /// Default: 1000 (1 second)
1354    #[serde(default = "crate::defaults::tmux_status_bar_refresh_ms")]
1355    pub tmux_status_bar_refresh_ms: u64,
1356
1357    /// Tmux prefix key for control mode
1358    /// In control mode, par-term intercepts this key combination and waits for a command key.
1359    /// Format: "C-b" (Ctrl+B, default), "C-Space" (Ctrl+Space), "C-a" (Ctrl+A), etc.
1360    /// The prefix + command key is translated to the appropriate tmux command.
1361    #[serde(default = "crate::defaults::tmux_prefix_key")]
1362    pub tmux_prefix_key: String,
1363
1364    /// Use native tmux format strings for status bar content
1365    /// When true, queries tmux for the actual status-left and status-right values
1366    /// using `display-message -p '#{T:status-left}'` command.
1367    /// When false, uses par-term's configurable format strings below.
1368    #[serde(default = "crate::defaults::bool_false")]
1369    pub tmux_status_bar_use_native_format: bool,
1370
1371    /// Tmux status bar left side format string.
1372    ///
1373    /// Supported variables:
1374    /// - `{session}` - Session name
1375    /// - `{windows}` - Window list with active marker (*)
1376    /// - `{pane}` - Focused pane ID
1377    /// - `{time:FORMAT}` - Current time with strftime format (e.g., `{time:%H:%M}`)
1378    /// - `{hostname}` - Machine hostname
1379    /// - `{user}` - Current username
1380    ///
1381    /// Default: `[{session}] {windows}`
1382    #[serde(default = "crate::defaults::tmux_status_bar_left")]
1383    pub tmux_status_bar_left: String,
1384
1385    /// Tmux status bar right side format string.
1386    ///
1387    /// Same variables as `tmux_status_bar_left`.
1388    ///
1389    /// Default: `{pane} | {time:%H:%M}`
1390    #[serde(default = "crate::defaults::tmux_status_bar_right")]
1391    pub tmux_status_bar_right: String,
1392
1393    // ========================================================================
1394    // Focus/Blur Power Saving
1395    // ========================================================================
1396    /// Pause shader animations when window loses focus
1397    /// This reduces GPU usage when the terminal is not actively being viewed
1398    #[serde(default = "crate::defaults::bool_true")]
1399    pub pause_shaders_on_blur: bool,
1400
1401    /// Reduce refresh rate when window is not focused
1402    /// When true, uses unfocused_fps instead of max_fps when window is blurred
1403    #[serde(default = "crate::defaults::bool_false")]
1404    pub pause_refresh_on_blur: bool,
1405
1406    /// Target FPS when window is not focused (only used if pause_refresh_on_blur is true)
1407    /// Lower values save more power but may delay terminal output visibility
1408    #[serde(default = "crate::defaults::unfocused_fps")]
1409    pub unfocused_fps: u32,
1410
1411    // ========================================================================
1412    // Shader Hot Reload
1413    // ========================================================================
1414    /// Enable automatic shader reloading when shader files are modified
1415    /// This watches custom_shader and cursor_shader files for changes
1416    #[serde(default = "crate::defaults::bool_false")]
1417    pub shader_hot_reload: bool,
1418
1419    /// Debounce delay in milliseconds before reloading shader after file change
1420    /// Helps avoid multiple reloads during rapid saves from editors
1421    #[serde(default = "crate::defaults::shader_hot_reload_delay")]
1422    pub shader_hot_reload_delay: u64,
1423
1424    // ========================================================================
1425    // Per-Shader Configuration Overrides
1426    // ========================================================================
1427    /// Per-shader configuration overrides (key = shader filename)
1428    /// These override settings embedded in shader metadata and global defaults
1429    #[serde(default)]
1430    pub shader_configs: HashMap<String, ShaderConfig>,
1431
1432    /// Per-cursor-shader configuration overrides (key = shader filename)
1433    #[serde(default)]
1434    pub cursor_shader_configs: HashMap<String, CursorShaderConfig>,
1435
1436    // ========================================================================
1437    // Keybindings
1438    // ========================================================================
1439    /// Custom keybindings (checked before built-in shortcuts)
1440    /// Format: key = "CmdOrCtrl+Shift+B", action = "toggle_background_shader"
1441    #[serde(default = "crate::defaults::keybindings")]
1442    pub keybindings: Vec<KeyBinding>,
1443
1444    // ========================================================================
1445    // Shader Installation
1446    // ========================================================================
1447    /// Shader install prompt preference
1448    /// - ask: Prompt user to install shaders if folder is missing/empty (default)
1449    /// - never: User declined, don't ask again
1450    /// - installed: Shaders have been installed
1451    #[serde(default)]
1452    pub shader_install_prompt: ShaderInstallPrompt,
1453
1454    /// Shell integration install state
1455    #[serde(default)]
1456    pub shell_integration_state: InstallPromptState,
1457
1458    /// Version tracking for integrations
1459    #[serde(default)]
1460    pub integration_versions: IntegrationVersions,
1461
1462    // ========================================================================
1463    // Update Checking
1464    // ========================================================================
1465    /// How often to check for new par-term releases
1466    /// - never: Disable automatic update checks
1467    /// - daily: Check once per day
1468    /// - weekly: Check once per week (default)
1469    /// - monthly: Check once per month
1470    #[serde(default = "crate::defaults::update_check_frequency")]
1471    pub update_check_frequency: UpdateCheckFrequency,
1472
1473    /// ISO 8601 timestamp of the last update check (auto-managed)
1474    #[serde(default)]
1475    pub last_update_check: Option<String>,
1476
1477    /// Version that user chose to skip notifications for
1478    #[serde(default)]
1479    pub skipped_version: Option<String>,
1480
1481    /// Last version we notified the user about (prevents repeat notifications)
1482    #[serde(default)]
1483    pub last_notified_version: Option<String>,
1484
1485    // ========================================================================
1486    // Window Arrangements
1487    // ========================================================================
1488    /// Name of arrangement to auto-restore on startup (None = disabled)
1489    #[serde(default, skip_serializing_if = "Option::is_none")]
1490    pub auto_restore_arrangement: Option<String>,
1491
1492    /// Whether to restore the previous session (tabs, panes, CWDs) on startup
1493    #[serde(default = "crate::defaults::bool_false")]
1494    pub restore_session: bool,
1495
1496    /// Seconds to keep closed tab metadata for undo (0 = disabled)
1497    #[serde(default = "crate::defaults::session_undo_timeout_secs")]
1498    pub session_undo_timeout_secs: u32,
1499
1500    /// Maximum number of closed tabs to remember for undo
1501    #[serde(default = "crate::defaults::session_undo_max_entries")]
1502    pub session_undo_max_entries: usize,
1503
1504    /// When true, closing a tab hides the shell instead of killing it.
1505    /// Undo restores the full session with scrollback and running processes.
1506    #[serde(default = "crate::defaults::session_undo_preserve_shell")]
1507    pub session_undo_preserve_shell: bool,
1508
1509    // ========================================================================
1510    // Search Settings
1511    // ========================================================================
1512    /// Highlight color for search matches [R, G, B, A] (0-255)
1513    #[serde(default = "crate::defaults::search_highlight_color")]
1514    pub search_highlight_color: [u8; 4],
1515
1516    /// Highlight color for the current/active search match [R, G, B, A] (0-255)
1517    #[serde(default = "crate::defaults::search_current_highlight_color")]
1518    pub search_current_highlight_color: [u8; 4],
1519
1520    /// Default case sensitivity for search
1521    #[serde(default = "crate::defaults::bool_false")]
1522    pub search_case_sensitive: bool,
1523
1524    /// Default regex mode for search
1525    #[serde(default = "crate::defaults::bool_false")]
1526    pub search_regex: bool,
1527
1528    /// Wrap around when navigating search matches
1529    #[serde(default = "crate::defaults::bool_true")]
1530    pub search_wrap_around: bool,
1531
1532    // ========================================================================
1533    // Session Logging
1534    // ========================================================================
1535    /// Automatically record all terminal sessions
1536    /// When enabled, all terminal output is logged to files in the log directory
1537    #[serde(default = "crate::defaults::bool_false")]
1538    pub auto_log_sessions: bool,
1539
1540    /// Log format for session recording
1541    /// - plain: Simple text output without escape sequences
1542    /// - html: Rendered output with colors preserved
1543    /// - asciicast: asciinema-compatible format for replay/sharing (default)
1544    #[serde(default)]
1545    pub session_log_format: SessionLogFormat,
1546
1547    /// Directory where session logs are saved
1548    /// Default: ~/.local/share/par-term/logs/
1549    #[serde(default = "crate::defaults::session_log_directory")]
1550    pub session_log_directory: String,
1551
1552    /// Automatically save session log when tab/window closes
1553    /// When true, ensures the session is fully written before the tab closes
1554    #[serde(default = "crate::defaults::bool_true")]
1555    pub archive_on_close: bool,
1556
1557    // ========================================================================
1558    // Debug Logging
1559    // ========================================================================
1560    /// Log level for debug log file output.
1561    /// Controls verbosity of `/tmp/par_term_debug.log`.
1562    /// Environment variable RUST_LOG and --log-level CLI flag take precedence.
1563    #[serde(default)]
1564    pub log_level: LogLevel,
1565
1566    // ========================================================================
1567    // Badge Settings (iTerm2-style session labels)
1568    // ========================================================================
1569    /// Enable badge display
1570    #[serde(default = "crate::defaults::bool_false")]
1571    pub badge_enabled: bool,
1572
1573    /// Badge text format with variable interpolation
1574    /// Supports \(session.username), \(session.hostname), \(session.path), etc.
1575    #[serde(default = "crate::defaults::badge_format")]
1576    pub badge_format: String,
1577
1578    /// Badge text color [R, G, B] (0-255)
1579    #[serde(default = "crate::defaults::badge_color")]
1580    pub badge_color: [u8; 3],
1581
1582    /// Badge opacity (0.0-1.0)
1583    #[serde(default = "crate::defaults::badge_color_alpha")]
1584    pub badge_color_alpha: f32,
1585
1586    /// Badge font family (uses system font if not found)
1587    #[serde(default = "crate::defaults::badge_font")]
1588    pub badge_font: String,
1589
1590    /// Use bold weight for badge font
1591    #[serde(default = "crate::defaults::bool_true")]
1592    pub badge_font_bold: bool,
1593
1594    /// Top margin in pixels from terminal edge
1595    #[serde(default = "crate::defaults::badge_top_margin")]
1596    pub badge_top_margin: f32,
1597
1598    /// Right margin in pixels from terminal edge
1599    #[serde(default = "crate::defaults::badge_right_margin")]
1600    pub badge_right_margin: f32,
1601
1602    /// Maximum badge width as fraction of terminal width (0.0-1.0)
1603    #[serde(default = "crate::defaults::badge_max_width")]
1604    pub badge_max_width: f32,
1605
1606    /// Maximum badge height as fraction of terminal height (0.0-1.0)
1607    #[serde(default = "crate::defaults::badge_max_height")]
1608    pub badge_max_height: f32,
1609
1610    // ========================================================================
1611    // Status Bar Settings
1612    // ========================================================================
1613    /// Enable the status bar
1614    #[serde(default = "crate::defaults::bool_false")]
1615    pub status_bar_enabled: bool,
1616
1617    /// Status bar position (top or bottom)
1618    #[serde(default)]
1619    pub status_bar_position: StatusBarPosition,
1620
1621    /// Status bar height in pixels
1622    #[serde(default = "crate::defaults::status_bar_height")]
1623    pub status_bar_height: f32,
1624
1625    /// Status bar background color [R, G, B] (0-255)
1626    #[serde(default = "crate::defaults::status_bar_bg_color")]
1627    pub status_bar_bg_color: [u8; 3],
1628
1629    /// Status bar background alpha (0.0-1.0)
1630    #[serde(default = "crate::defaults::status_bar_bg_alpha")]
1631    pub status_bar_bg_alpha: f32,
1632
1633    /// Status bar foreground (text) color [R, G, B] (0-255)
1634    #[serde(default = "crate::defaults::status_bar_fg_color")]
1635    pub status_bar_fg_color: [u8; 3],
1636
1637    /// Status bar font family (empty string = use terminal font)
1638    #[serde(default)]
1639    pub status_bar_font: String,
1640
1641    /// Status bar font size in points
1642    #[serde(default = "crate::defaults::status_bar_font_size")]
1643    pub status_bar_font_size: f32,
1644
1645    /// Separator string between widgets
1646    #[serde(default = "crate::defaults::status_bar_separator")]
1647    pub status_bar_separator: String,
1648
1649    /// Auto-hide the status bar when in fullscreen mode
1650    #[serde(default = "crate::defaults::bool_true")]
1651    pub status_bar_auto_hide_fullscreen: bool,
1652
1653    /// Auto-hide the status bar when mouse is inactive
1654    #[serde(default = "crate::defaults::bool_false")]
1655    pub status_bar_auto_hide_mouse_inactive: bool,
1656
1657    /// Timeout in seconds before hiding status bar after last mouse activity
1658    #[serde(default = "crate::defaults::status_bar_mouse_inactive_timeout")]
1659    pub status_bar_mouse_inactive_timeout: f32,
1660
1661    /// Polling interval in seconds for system monitor data (CPU, memory, network)
1662    #[serde(default = "crate::defaults::status_bar_system_poll_interval")]
1663    pub status_bar_system_poll_interval: f32,
1664
1665    /// Polling interval in seconds for git branch detection
1666    #[serde(default = "crate::defaults::status_bar_git_poll_interval")]
1667    pub status_bar_git_poll_interval: f32,
1668
1669    /// Time format string for the Clock widget (chrono strftime syntax)
1670    #[serde(default = "crate::defaults::status_bar_time_format")]
1671    pub status_bar_time_format: String,
1672
1673    /// Show ahead/behind and dirty indicators on the Git Branch widget
1674    #[serde(default = "crate::defaults::bool_true")]
1675    pub status_bar_git_show_status: bool,
1676
1677    /// Widget configuration list
1678    #[serde(default = "crate::status_bar::default_widgets")]
1679    pub status_bar_widgets: Vec<crate::status_bar::StatusBarWidgetConfig>,
1680
1681    // ========================================================================
1682    // Progress Bar Settings (OSC 9;4 and OSC 934)
1683    // ========================================================================
1684    /// Enable progress bar overlay
1685    /// When enabled, progress bars from OSC 9;4 and OSC 934 sequences are displayed
1686    #[serde(default = "crate::defaults::bool_true")]
1687    pub progress_bar_enabled: bool,
1688
1689    /// Progress bar visual style
1690    /// - bar: Simple thin bar (default)
1691    /// - bar_with_text: Bar with percentage text and labels
1692    #[serde(default)]
1693    pub progress_bar_style: ProgressBarStyle,
1694
1695    /// Progress bar position
1696    /// - bottom: Display at the bottom of the terminal (default)
1697    /// - top: Display at the top of the terminal
1698    #[serde(default)]
1699    pub progress_bar_position: ProgressBarPosition,
1700
1701    /// Progress bar height in pixels
1702    #[serde(default = "crate::defaults::progress_bar_height")]
1703    pub progress_bar_height: f32,
1704
1705    /// Progress bar opacity (0.0-1.0)
1706    #[serde(default = "crate::defaults::progress_bar_opacity")]
1707    pub progress_bar_opacity: f32,
1708
1709    /// Color for normal progress state [R, G, B] (0-255)
1710    #[serde(default = "crate::defaults::progress_bar_normal_color")]
1711    pub progress_bar_normal_color: [u8; 3],
1712
1713    /// Color for warning progress state [R, G, B] (0-255)
1714    #[serde(default = "crate::defaults::progress_bar_warning_color")]
1715    pub progress_bar_warning_color: [u8; 3],
1716
1717    /// Color for error progress state [R, G, B] (0-255)
1718    #[serde(default = "crate::defaults::progress_bar_error_color")]
1719    pub progress_bar_error_color: [u8; 3],
1720
1721    /// Color for indeterminate progress state [R, G, B] (0-255)
1722    #[serde(default = "crate::defaults::progress_bar_indeterminate_color")]
1723    pub progress_bar_indeterminate_color: [u8; 3],
1724
1725    // ========================================================================
1726    // Triggers & Automation
1727    // ========================================================================
1728    /// Regex trigger definitions that match terminal output and fire actions
1729    #[serde(default)]
1730    pub triggers: Vec<crate::automation::TriggerConfig>,
1731
1732    /// Coprocess definitions for piped subprocess management
1733    #[serde(default)]
1734    pub coprocesses: Vec<crate::automation::CoprocessDefConfig>,
1735
1736    /// External observer script definitions
1737    #[serde(default)]
1738    pub scripts: Vec<crate::scripting::ScriptConfig>,
1739
1740    // ========================================================================
1741    // Snippets & Actions
1742    // ========================================================================
1743    /// Text snippets for quick insertion
1744    #[serde(default)]
1745    pub snippets: Vec<SnippetConfig>,
1746
1747    /// Custom actions (shell commands, text insertion, key sequences)
1748    #[serde(default)]
1749    pub actions: Vec<CustomActionConfig>,
1750
1751    // ========================================================================
1752    // UI State (persisted across sessions)
1753    // ========================================================================
1754    /// Settings window section IDs that have been toggled from their default collapse state.
1755    /// Sections default to open unless specified otherwise; IDs in this set invert the default.
1756    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1757    pub collapsed_settings_sections: Vec<String>,
1758
1759    // ========================================================================
1760    // Dynamic Profile Sources
1761    // ========================================================================
1762    /// Remote URLs to fetch profile definitions from
1763    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1764    pub dynamic_profile_sources: Vec<crate::profile::DynamicProfileSource>,
1765
1766    // ========================================================================
1767    // AI Inspector
1768    // ========================================================================
1769    /// Enable AI Inspector side panel
1770    #[serde(default = "crate::defaults::ai_inspector_enabled")]
1771    pub ai_inspector_enabled: bool,
1772
1773    /// Open the AI Inspector panel automatically on startup
1774    #[serde(default = "crate::defaults::ai_inspector_open_on_startup")]
1775    pub ai_inspector_open_on_startup: bool,
1776
1777    /// Width of the AI Inspector panel in pixels
1778    #[serde(default = "crate::defaults::ai_inspector_width")]
1779    pub ai_inspector_width: f32,
1780
1781    /// Default capture scope: "visible", "scrollback", or "selection"
1782    #[serde(default = "crate::defaults::ai_inspector_default_scope")]
1783    pub ai_inspector_default_scope: String,
1784
1785    /// View mode for inspector results: "cards" or "raw"
1786    #[serde(default = "crate::defaults::ai_inspector_view_mode")]
1787    pub ai_inspector_view_mode: String,
1788
1789    /// Automatically refresh inspector when terminal content changes
1790    #[serde(default = "crate::defaults::ai_inspector_live_update")]
1791    pub ai_inspector_live_update: bool,
1792
1793    /// Show semantic zone overlays on terminal content
1794    #[serde(default = "crate::defaults::ai_inspector_show_zones")]
1795    pub ai_inspector_show_zones: bool,
1796
1797    /// AI agent identifier for inspector queries
1798    #[serde(default = "crate::defaults::ai_inspector_agent")]
1799    pub ai_inspector_agent: String,
1800
1801    /// Automatically launch AI agent when inspector opens
1802    #[serde(default = "crate::defaults::ai_inspector_auto_launch")]
1803    pub ai_inspector_auto_launch: bool,
1804
1805    /// Automatically include terminal context with AI queries
1806    #[serde(default = "crate::defaults::ai_inspector_auto_context")]
1807    pub ai_inspector_auto_context: bool,
1808
1809    /// Maximum number of terminal lines to include as AI context
1810    #[serde(default = "crate::defaults::ai_inspector_context_max_lines")]
1811    pub ai_inspector_context_max_lines: usize,
1812
1813    /// Automatically approve AI-suggested actions without confirmation
1814    #[serde(default = "crate::defaults::ai_inspector_auto_approve")]
1815    pub ai_inspector_auto_approve: bool,
1816
1817    /// Allow the AI agent to write input to the terminal (drive terminal)
1818    #[serde(default = "crate::defaults::ai_inspector_agent_terminal_access")]
1819    pub ai_inspector_agent_terminal_access: bool,
1820}
1821
1822impl Default for Config {
1823    fn default() -> Self {
1824        Self {
1825            cols: crate::defaults::cols(),
1826            rows: crate::defaults::rows(),
1827            font_size: crate::defaults::font_size(),
1828            font_family: crate::defaults::font_family(),
1829            font_family_bold: None,
1830            font_family_italic: None,
1831            font_family_bold_italic: None,
1832            font_ranges: Vec::new(),
1833            line_spacing: crate::defaults::line_spacing(),
1834            char_spacing: crate::defaults::char_spacing(),
1835            enable_text_shaping: crate::defaults::text_shaping(),
1836            enable_ligatures: crate::defaults::bool_true(),
1837            enable_kerning: crate::defaults::bool_true(),
1838            font_antialias: crate::defaults::bool_true(),
1839            font_hinting: false,
1840            font_thin_strokes: ThinStrokesMode::default(),
1841            minimum_contrast: crate::defaults::minimum_contrast(),
1842            copy_mode_enabled: crate::defaults::bool_true(),
1843            copy_mode_auto_exit_on_yank: crate::defaults::bool_true(),
1844            copy_mode_show_status: crate::defaults::bool_true(),
1845            scrollback_lines: crate::defaults::scrollback(),
1846            unicode_version: crate::defaults::unicode_version(),
1847            ambiguous_width: crate::defaults::ambiguous_width(),
1848            normalization_form: crate::defaults::normalization_form(),
1849            cursor_blink: crate::defaults::bool_false(),
1850            cursor_blink_interval: crate::defaults::cursor_blink_interval(),
1851            cursor_style: CursorStyle::default(),
1852            cursor_color: crate::defaults::cursor_color(),
1853            cursor_text_color: None,
1854            lock_cursor_visibility: crate::defaults::bool_false(),
1855            lock_cursor_style: crate::defaults::bool_false(),
1856            lock_cursor_blink: crate::defaults::bool_false(),
1857            cursor_guide_enabled: crate::defaults::bool_false(),
1858            cursor_guide_color: crate::defaults::cursor_guide_color(),
1859            cursor_shadow_enabled: crate::defaults::bool_false(),
1860            cursor_shadow_color: crate::defaults::cursor_shadow_color(),
1861            cursor_shadow_offset: crate::defaults::cursor_shadow_offset(),
1862            cursor_shadow_blur: crate::defaults::cursor_shadow_blur(),
1863            cursor_boost: crate::defaults::cursor_boost(),
1864            cursor_boost_color: crate::defaults::cursor_boost_color(),
1865            unfocused_cursor_style: UnfocusedCursorStyle::default(),
1866            scrollbar_autohide_delay: crate::defaults::scrollbar_autohide_delay(),
1867            window_title: crate::defaults::window_title(),
1868            allow_title_change: crate::defaults::bool_true(),
1869            theme: crate::defaults::theme(),
1870            auto_dark_mode: false,
1871            light_theme: crate::defaults::light_theme(),
1872            dark_theme: crate::defaults::dark_theme(),
1873            left_option_key_mode: OptionKeyMode::default(),
1874            right_option_key_mode: OptionKeyMode::default(),
1875            modifier_remapping: ModifierRemapping::default(),
1876            use_physical_keys: crate::defaults::bool_false(),
1877            auto_copy_selection: crate::defaults::bool_true(),
1878            copy_trailing_newline: crate::defaults::bool_false(),
1879            middle_click_paste: crate::defaults::bool_true(),
1880            paste_delay_ms: crate::defaults::paste_delay_ms(),
1881            dropped_file_quote_style: DroppedFileQuoteStyle::default(),
1882            mouse_scroll_speed: crate::defaults::scroll_speed(),
1883            mouse_double_click_threshold: crate::defaults::double_click_threshold(),
1884            mouse_triple_click_threshold: crate::defaults::triple_click_threshold(),
1885            option_click_moves_cursor: crate::defaults::bool_true(),
1886            focus_follows_mouse: crate::defaults::bool_false(),
1887            report_horizontal_scroll: crate::defaults::bool_true(),
1888            word_characters: crate::defaults::word_characters(),
1889            smart_selection_enabled: crate::defaults::smart_selection_enabled(),
1890            smart_selection_rules: default_smart_selection_rules(),
1891            screenshot_format: crate::defaults::screenshot_format(),
1892            max_fps: crate::defaults::max_fps(),
1893            vsync_mode: VsyncMode::default(),
1894            power_preference: PowerPreference::default(),
1895            reduce_flicker: crate::defaults::reduce_flicker(),
1896            reduce_flicker_delay_ms: crate::defaults::reduce_flicker_delay_ms(),
1897            maximize_throughput: crate::defaults::maximize_throughput(),
1898            throughput_render_interval_ms: crate::defaults::throughput_render_interval_ms(),
1899            window_padding: crate::defaults::window_padding(),
1900            hide_window_padding_on_split: crate::defaults::bool_true(),
1901            window_opacity: crate::defaults::window_opacity(),
1902            window_always_on_top: crate::defaults::bool_false(),
1903            window_decorations: crate::defaults::bool_true(),
1904            window_type: WindowType::default(),
1905            target_monitor: None,
1906            target_space: None,
1907            lock_window_size: crate::defaults::bool_false(),
1908            show_window_number: crate::defaults::bool_false(),
1909            transparency_affects_only_default_background: crate::defaults::bool_true(),
1910            keep_text_opaque: crate::defaults::bool_true(),
1911            blur_enabled: crate::defaults::bool_false(),
1912            blur_radius: crate::defaults::blur_radius(),
1913            background_image: None,
1914            background_image_enabled: crate::defaults::bool_true(),
1915            background_image_mode: BackgroundImageMode::default(),
1916            background_image_opacity: crate::defaults::background_image_opacity(),
1917            image_scaling_mode: ImageScalingMode::default(),
1918            image_preserve_aspect_ratio: crate::defaults::bool_true(),
1919            background_mode: BackgroundMode::default(),
1920            pane_backgrounds: Vec::new(),
1921            background_color: crate::defaults::background_color(),
1922            download_save_location: DownloadSaveLocation::default(),
1923            last_download_directory: None,
1924            custom_shader: None,
1925            custom_shader_enabled: crate::defaults::bool_true(),
1926            custom_shader_animation: crate::defaults::bool_true(),
1927            custom_shader_animation_speed: crate::defaults::custom_shader_speed(),
1928            custom_shader_text_opacity: crate::defaults::text_opacity(),
1929            custom_shader_full_content: crate::defaults::bool_false(),
1930            custom_shader_brightness: crate::defaults::custom_shader_brightness(),
1931            custom_shader_channel0: None,
1932            custom_shader_channel1: None,
1933            custom_shader_channel2: None,
1934            custom_shader_channel3: None,
1935            custom_shader_cubemap: None,
1936            custom_shader_cubemap_enabled: crate::defaults::cubemap_enabled(),
1937            custom_shader_use_background_as_channel0: crate::defaults::use_background_as_channel0(),
1938            cursor_shader: None,
1939            cursor_shader_enabled: crate::defaults::bool_false(),
1940            cursor_shader_animation: crate::defaults::bool_true(),
1941            cursor_shader_animation_speed: crate::defaults::custom_shader_speed(),
1942            cursor_shader_color: crate::defaults::cursor_shader_color(),
1943            cursor_shader_trail_duration: crate::defaults::cursor_trail_duration(),
1944            cursor_shader_glow_radius: crate::defaults::cursor_glow_radius(),
1945            cursor_shader_glow_intensity: crate::defaults::cursor_glow_intensity(),
1946            cursor_shader_hides_cursor: crate::defaults::bool_false(),
1947            cursor_shader_disable_in_alt_screen:
1948                crate::defaults::cursor_shader_disable_in_alt_screen(),
1949            shell_exit_action: ShellExitAction::default(),
1950            custom_shell: None,
1951            shell_args: None,
1952            working_directory: None,
1953            startup_directory_mode: StartupDirectoryMode::default(),
1954            startup_directory: None,
1955            last_working_directory: None,
1956            shell_env: None,
1957            login_shell: crate::defaults::login_shell(),
1958            initial_text: crate::defaults::initial_text(),
1959            initial_text_delay_ms: crate::defaults::initial_text_delay_ms(),
1960            initial_text_send_newline: crate::defaults::initial_text_send_newline(),
1961            answerback_string: crate::defaults::answerback_string(),
1962            prompt_on_quit: crate::defaults::bool_false(),
1963            confirm_close_running_jobs: crate::defaults::bool_false(),
1964            jobs_to_ignore: crate::defaults::jobs_to_ignore(),
1965            semantic_history_enabled: crate::defaults::bool_true(),
1966            semantic_history_editor_mode: SemanticHistoryEditorMode::default(),
1967            semantic_history_editor: crate::defaults::semantic_history_editor(),
1968            link_highlight_color: crate::defaults::link_highlight_color(),
1969            link_highlight_underline: crate::defaults::bool_true(),
1970            link_underline_style: crate::types::LinkUnderlineStyle::default(),
1971            link_handler_command: String::new(),
1972            scrollbar_position: crate::defaults::scrollbar_position(),
1973            scrollbar_width: crate::defaults::scrollbar_width(),
1974            scrollbar_thumb_color: crate::defaults::scrollbar_thumb_color(),
1975            scrollbar_track_color: crate::defaults::scrollbar_track_color(),
1976            scrollbar_command_marks: crate::defaults::bool_true(),
1977            scrollbar_mark_tooltips: crate::defaults::bool_false(),
1978            command_separator_enabled: crate::defaults::bool_false(),
1979            command_separator_thickness: crate::defaults::command_separator_thickness(),
1980            command_separator_opacity: crate::defaults::command_separator_opacity(),
1981            command_separator_exit_color: crate::defaults::bool_true(),
1982            command_separator_color: crate::defaults::command_separator_color(),
1983            clipboard_max_sync_events: crate::defaults::clipboard_max_sync_events(),
1984            clipboard_max_event_bytes: crate::defaults::clipboard_max_event_bytes(),
1985            command_history_max_entries: crate::defaults::command_history_max_entries(),
1986            notification_bell_desktop: crate::defaults::bool_false(),
1987            notification_bell_sound: crate::defaults::bell_sound(),
1988            notification_bell_visual: crate::defaults::bool_true(),
1989            notification_activity_enabled: crate::defaults::bool_false(),
1990            notification_activity_threshold: crate::defaults::activity_threshold(),
1991            anti_idle_enabled: crate::defaults::bool_false(),
1992            anti_idle_seconds: crate::defaults::anti_idle_seconds(),
1993            anti_idle_code: crate::defaults::anti_idle_code(),
1994            notification_silence_enabled: crate::defaults::bool_false(),
1995            notification_silence_threshold: crate::defaults::silence_threshold(),
1996            notification_session_ended: crate::defaults::bool_false(),
1997            suppress_notifications_when_focused: crate::defaults::bool_true(),
1998            notification_max_buffer: crate::defaults::notification_max_buffer(),
1999            alert_sounds: HashMap::new(),
2000            enable_mdns_discovery: crate::defaults::bool_false(),
2001            mdns_scan_timeout_secs: crate::defaults::mdns_timeout(),
2002            ssh_auto_profile_switch: crate::defaults::bool_true(),
2003            ssh_revert_profile_on_disconnect: crate::defaults::bool_true(),
2004            tab_style: TabStyle::default(),
2005            light_tab_style: crate::defaults::light_tab_style(),
2006            dark_tab_style: crate::defaults::dark_tab_style(),
2007            tab_bar_mode: TabBarMode::default(),
2008            tab_bar_height: crate::defaults::tab_bar_height(),
2009            tab_bar_position: TabBarPosition::default(),
2010            tab_bar_width: crate::defaults::tab_bar_width(),
2011            tab_show_close_button: crate::defaults::bool_true(),
2012            tab_show_index: crate::defaults::bool_false(),
2013            tab_inherit_cwd: crate::defaults::bool_true(),
2014            max_tabs: crate::defaults::zero(),
2015            show_profile_drawer_button: crate::defaults::bool_false(),
2016            new_tab_shortcut_shows_profiles: crate::defaults::bool_false(),
2017            tab_bar_background: crate::defaults::tab_bar_background(),
2018            tab_active_background: crate::defaults::tab_active_background(),
2019            tab_inactive_background: crate::defaults::tab_inactive_background(),
2020            tab_hover_background: crate::defaults::tab_hover_background(),
2021            tab_active_text: crate::defaults::tab_active_text(),
2022            tab_inactive_text: crate::defaults::tab_inactive_text(),
2023            tab_active_indicator: crate::defaults::tab_active_indicator(),
2024            tab_activity_indicator: crate::defaults::tab_activity_indicator(),
2025            tab_bell_indicator: crate::defaults::tab_bell_indicator(),
2026            tab_close_button: crate::defaults::tab_close_button(),
2027            tab_close_button_hover: crate::defaults::tab_close_button_hover(),
2028            dim_inactive_tabs: crate::defaults::bool_true(),
2029            inactive_tab_opacity: crate::defaults::inactive_tab_opacity(),
2030            tab_min_width: crate::defaults::tab_min_width(),
2031            tab_stretch_to_fill: crate::defaults::tab_stretch_to_fill(),
2032            tab_html_titles: crate::defaults::tab_html_titles(),
2033            tab_border_color: crate::defaults::tab_border_color(),
2034            tab_border_width: crate::defaults::tab_border_width(),
2035            tab_inactive_outline_only: crate::defaults::bool_false(),
2036            // Split panes
2037            pane_divider_width: crate::defaults::pane_divider_width(),
2038            pane_divider_hit_width: crate::defaults::pane_divider_hit_width(),
2039            pane_padding: crate::defaults::pane_padding(),
2040            pane_min_size: crate::defaults::pane_min_size(),
2041            pane_background_opacity: crate::defaults::pane_background_opacity(),
2042            pane_divider_color: crate::defaults::pane_divider_color(),
2043            pane_divider_hover_color: crate::defaults::pane_divider_hover_color(),
2044            dim_inactive_panes: crate::defaults::bool_false(),
2045            inactive_pane_opacity: crate::defaults::inactive_pane_opacity(),
2046            show_pane_titles: crate::defaults::bool_false(),
2047            pane_title_height: crate::defaults::pane_title_height(),
2048            pane_title_position: PaneTitlePosition::default(),
2049            pane_title_color: crate::defaults::pane_title_color(),
2050            pane_title_bg_color: crate::defaults::pane_title_bg_color(),
2051            pane_title_font: String::new(),
2052            pane_divider_style: DividerStyle::default(),
2053            max_panes: crate::defaults::max_panes(),
2054            pane_focus_indicator: crate::defaults::bool_true(),
2055            pane_focus_color: crate::defaults::pane_focus_color(),
2056            pane_focus_width: crate::defaults::pane_focus_width(),
2057            tmux_enabled: crate::defaults::bool_false(),
2058            tmux_path: crate::defaults::tmux_path(),
2059            tmux_default_session: crate::defaults::tmux_default_session(),
2060            tmux_auto_attach: crate::defaults::bool_false(),
2061            tmux_auto_attach_session: crate::defaults::tmux_auto_attach_session(),
2062            tmux_clipboard_sync: crate::defaults::bool_true(),
2063            tmux_profile: None,
2064            tmux_show_status_bar: crate::defaults::bool_false(),
2065            tmux_status_bar_refresh_ms: crate::defaults::tmux_status_bar_refresh_ms(),
2066            tmux_prefix_key: crate::defaults::tmux_prefix_key(),
2067            tmux_status_bar_use_native_format: crate::defaults::bool_false(),
2068            tmux_status_bar_left: crate::defaults::tmux_status_bar_left(),
2069            tmux_status_bar_right: crate::defaults::tmux_status_bar_right(),
2070            pause_shaders_on_blur: crate::defaults::bool_true(),
2071            pause_refresh_on_blur: crate::defaults::bool_false(),
2072            unfocused_fps: crate::defaults::unfocused_fps(),
2073            shader_hot_reload: crate::defaults::bool_false(),
2074            shader_hot_reload_delay: crate::defaults::shader_hot_reload_delay(),
2075            shader_configs: HashMap::new(),
2076            cursor_shader_configs: HashMap::new(),
2077            keybindings: crate::defaults::keybindings(),
2078            shader_install_prompt: ShaderInstallPrompt::default(),
2079            shell_integration_state: InstallPromptState::default(),
2080            integration_versions: IntegrationVersions::default(),
2081            update_check_frequency: crate::defaults::update_check_frequency(),
2082            last_update_check: None,
2083            skipped_version: None,
2084            last_notified_version: None,
2085            auto_restore_arrangement: None,
2086            restore_session: crate::defaults::bool_false(),
2087            session_undo_timeout_secs: crate::defaults::session_undo_timeout_secs(),
2088            session_undo_max_entries: crate::defaults::session_undo_max_entries(),
2089            session_undo_preserve_shell: crate::defaults::session_undo_preserve_shell(),
2090            search_highlight_color: crate::defaults::search_highlight_color(),
2091            search_current_highlight_color: crate::defaults::search_current_highlight_color(),
2092            search_case_sensitive: crate::defaults::bool_false(),
2093            search_regex: crate::defaults::bool_false(),
2094            search_wrap_around: crate::defaults::bool_true(),
2095            // Session logging
2096            auto_log_sessions: crate::defaults::bool_false(),
2097            session_log_format: SessionLogFormat::default(),
2098            session_log_directory: crate::defaults::session_log_directory(),
2099            archive_on_close: crate::defaults::bool_true(),
2100            // Debug Logging
2101            log_level: LogLevel::default(),
2102            // Badge
2103            badge_enabled: crate::defaults::bool_false(),
2104            badge_format: crate::defaults::badge_format(),
2105            badge_color: crate::defaults::badge_color(),
2106            badge_color_alpha: crate::defaults::badge_color_alpha(),
2107            badge_font: crate::defaults::badge_font(),
2108            badge_font_bold: crate::defaults::bool_true(),
2109            badge_top_margin: crate::defaults::badge_top_margin(),
2110            badge_right_margin: crate::defaults::badge_right_margin(),
2111            badge_max_width: crate::defaults::badge_max_width(),
2112            badge_max_height: crate::defaults::badge_max_height(),
2113            // Status Bar
2114            status_bar_enabled: crate::defaults::bool_false(),
2115            status_bar_position: StatusBarPosition::default(),
2116            status_bar_height: crate::defaults::status_bar_height(),
2117            status_bar_bg_color: crate::defaults::status_bar_bg_color(),
2118            status_bar_bg_alpha: crate::defaults::status_bar_bg_alpha(),
2119            status_bar_fg_color: crate::defaults::status_bar_fg_color(),
2120            status_bar_font: String::new(),
2121            status_bar_font_size: crate::defaults::status_bar_font_size(),
2122            status_bar_separator: crate::defaults::status_bar_separator(),
2123            status_bar_auto_hide_fullscreen: crate::defaults::bool_true(),
2124            status_bar_auto_hide_mouse_inactive: crate::defaults::bool_false(),
2125            status_bar_mouse_inactive_timeout: crate::defaults::status_bar_mouse_inactive_timeout(),
2126            status_bar_system_poll_interval: crate::defaults::status_bar_system_poll_interval(),
2127            status_bar_git_poll_interval: crate::defaults::status_bar_git_poll_interval(),
2128            status_bar_time_format: crate::defaults::status_bar_time_format(),
2129            status_bar_git_show_status: crate::defaults::bool_true(),
2130            status_bar_widgets: crate::status_bar::default_widgets(),
2131            // Progress Bar
2132            progress_bar_enabled: crate::defaults::bool_true(),
2133            progress_bar_style: ProgressBarStyle::default(),
2134            progress_bar_position: ProgressBarPosition::default(),
2135            progress_bar_height: crate::defaults::progress_bar_height(),
2136            progress_bar_opacity: crate::defaults::progress_bar_opacity(),
2137            progress_bar_normal_color: crate::defaults::progress_bar_normal_color(),
2138            progress_bar_warning_color: crate::defaults::progress_bar_warning_color(),
2139            progress_bar_error_color: crate::defaults::progress_bar_error_color(),
2140            progress_bar_indeterminate_color: crate::defaults::progress_bar_indeterminate_color(),
2141            triggers: Vec::new(),
2142            coprocesses: Vec::new(),
2143            scripts: Vec::new(),
2144            snippets: Vec::new(),
2145            actions: Vec::new(),
2146            collapsed_settings_sections: Vec::new(),
2147            dynamic_profile_sources: Vec::new(),
2148            // AI Inspector
2149            ai_inspector_enabled: crate::defaults::ai_inspector_enabled(),
2150            ai_inspector_open_on_startup: crate::defaults::ai_inspector_open_on_startup(),
2151            ai_inspector_width: crate::defaults::ai_inspector_width(),
2152            ai_inspector_default_scope: crate::defaults::ai_inspector_default_scope(),
2153            ai_inspector_view_mode: crate::defaults::ai_inspector_view_mode(),
2154            ai_inspector_live_update: crate::defaults::ai_inspector_live_update(),
2155            ai_inspector_show_zones: crate::defaults::ai_inspector_show_zones(),
2156            ai_inspector_agent: crate::defaults::ai_inspector_agent(),
2157            ai_inspector_auto_launch: crate::defaults::ai_inspector_auto_launch(),
2158            ai_inspector_auto_context: crate::defaults::ai_inspector_auto_context(),
2159            ai_inspector_context_max_lines: crate::defaults::ai_inspector_context_max_lines(),
2160            ai_inspector_auto_approve: crate::defaults::ai_inspector_auto_approve(),
2161            ai_inspector_agent_terminal_access: crate::defaults::ai_inspector_agent_terminal_access(
2162            ),
2163        }
2164    }
2165}
2166
2167impl Config {
2168    /// Apply tab style preset, overwriting the tab bar color/size fields.
2169    ///
2170    /// This is called when the user changes `tab_style` in settings.
2171    /// The `Dark` style corresponds to the existing defaults and does nothing.
2172    pub fn apply_tab_style(&mut self) {
2173        match self.tab_style {
2174            TabStyle::Dark => {
2175                // Default dark theme - restore original defaults
2176                self.tab_bar_background = crate::defaults::tab_bar_background();
2177                self.tab_active_background = crate::defaults::tab_active_background();
2178                self.tab_inactive_background = crate::defaults::tab_inactive_background();
2179                self.tab_hover_background = crate::defaults::tab_hover_background();
2180                self.tab_active_text = crate::defaults::tab_active_text();
2181                self.tab_inactive_text = crate::defaults::tab_inactive_text();
2182                self.tab_active_indicator = crate::defaults::tab_active_indicator();
2183                self.tab_border_color = crate::defaults::tab_border_color();
2184                self.tab_border_width = crate::defaults::tab_border_width();
2185                self.tab_inactive_outline_only = false;
2186                self.tab_bar_height = crate::defaults::tab_bar_height();
2187            }
2188            TabStyle::Light => {
2189                self.tab_bar_background = [235, 235, 235];
2190                self.tab_active_background = [255, 255, 255];
2191                self.tab_inactive_background = [225, 225, 225];
2192                self.tab_hover_background = [240, 240, 240];
2193                self.tab_active_text = [30, 30, 30];
2194                self.tab_inactive_text = [100, 100, 100];
2195                self.tab_active_indicator = [50, 120, 220];
2196                self.tab_border_color = [200, 200, 200];
2197                self.tab_border_width = 1.0;
2198                self.tab_inactive_outline_only = false;
2199                self.tab_bar_height = crate::defaults::tab_bar_height();
2200            }
2201            TabStyle::Compact => {
2202                // Smaller tabs, tighter spacing
2203                self.tab_bar_background = [35, 35, 35];
2204                self.tab_active_background = [55, 55, 55];
2205                self.tab_inactive_background = [35, 35, 35];
2206                self.tab_hover_background = [45, 45, 45];
2207                self.tab_active_text = [240, 240, 240];
2208                self.tab_inactive_text = [160, 160, 160];
2209                self.tab_active_indicator = [80, 140, 240];
2210                self.tab_border_color = [60, 60, 60];
2211                self.tab_border_width = 0.5;
2212                self.tab_inactive_outline_only = false;
2213                self.tab_bar_height = 22.0;
2214            }
2215            TabStyle::Minimal => {
2216                // Very clean, flat look with minimal decoration
2217                self.tab_bar_background = [30, 30, 30];
2218                self.tab_active_background = [30, 30, 30];
2219                self.tab_inactive_background = [30, 30, 30];
2220                self.tab_hover_background = [40, 40, 40];
2221                self.tab_active_text = [255, 255, 255];
2222                self.tab_inactive_text = [120, 120, 120];
2223                self.tab_active_indicator = [100, 150, 255];
2224                self.tab_border_color = [30, 30, 30]; // No visible border
2225                self.tab_border_width = 0.0;
2226                self.tab_inactive_outline_only = false;
2227                self.tab_bar_height = 26.0;
2228            }
2229            TabStyle::HighContrast => {
2230                // Maximum contrast for accessibility
2231                self.tab_bar_background = [0, 0, 0];
2232                self.tab_active_background = [255, 255, 255];
2233                self.tab_inactive_background = [30, 30, 30];
2234                self.tab_hover_background = [60, 60, 60];
2235                self.tab_active_text = [0, 0, 0];
2236                self.tab_inactive_text = [255, 255, 255];
2237                self.tab_active_indicator = [255, 255, 0];
2238                self.tab_border_color = [255, 255, 255];
2239                self.tab_border_width = 2.0;
2240                self.tab_inactive_outline_only = false;
2241                self.tab_bar_height = 30.0;
2242            }
2243            TabStyle::Automatic => {
2244                // No-op here: actual style is resolved and applied by apply_system_tab_style()
2245            }
2246        }
2247    }
2248
2249    /// Load configuration from file or create default
2250    pub fn load() -> Result<Self> {
2251        let config_path = Self::config_path();
2252        log::info!("Config path: {:?}", config_path);
2253
2254        if config_path.exists() {
2255            log::info!("Loading existing config from {:?}", config_path);
2256            let contents = fs::read_to_string(&config_path)?;
2257            let contents = substitute_variables(&contents);
2258            let mut config: Config = serde_yaml::from_str(&contents)?;
2259
2260            // Merge in any new default keybindings that don't exist in user's config
2261            config.merge_default_keybindings();
2262
2263            // Merge in any new default status bar widgets that don't exist in user's config
2264            config.merge_default_widgets();
2265
2266            // Generate keybindings for snippets and actions
2267            config.generate_snippet_action_keybindings();
2268
2269            // Load last working directory from state file (for "previous session" mode)
2270            config.load_last_working_directory();
2271
2272            Ok(config)
2273        } else {
2274            log::info!(
2275                "Config file not found, creating default at {:?}",
2276                config_path
2277            );
2278            // Create default config and save it
2279            let mut config = Self::default();
2280            // Generate keybindings for snippets and actions
2281            config.generate_snippet_action_keybindings();
2282            if let Err(e) = config.save() {
2283                log::error!("Failed to save default config: {}", e);
2284                return Err(e);
2285            }
2286
2287            // Load last working directory from state file (for "previous session" mode)
2288            config.load_last_working_directory();
2289
2290            log::info!("Default config created successfully");
2291            Ok(config)
2292        }
2293    }
2294
2295    /// Merge default keybindings into the user's config.
2296    /// Only adds keybindings for actions that don't already exist in the user's config.
2297    /// This ensures new features with default keybindings are available to existing users.
2298    fn merge_default_keybindings(&mut self) {
2299        let default_keybindings = crate::defaults::keybindings();
2300
2301        // Get the set of actions already configured by the user (owned strings to avoid borrow issues)
2302        let existing_actions: std::collections::HashSet<String> = self
2303            .keybindings
2304            .iter()
2305            .map(|kb| kb.action.clone())
2306            .collect();
2307
2308        // Add any default keybindings whose actions are not already configured
2309        let mut added_count = 0;
2310        for default_kb in default_keybindings {
2311            if !existing_actions.contains(&default_kb.action) {
2312                log::info!(
2313                    "Adding new default keybinding: {} -> {}",
2314                    default_kb.key,
2315                    default_kb.action
2316                );
2317                self.keybindings.push(default_kb);
2318                added_count += 1;
2319            }
2320        }
2321
2322        if added_count > 0 {
2323            log::info!(
2324                "Merged {} new default keybinding(s) into user config",
2325                added_count
2326            );
2327        }
2328    }
2329
2330    /// Merge default status bar widgets into the user's config.
2331    /// Only adds widgets whose `WidgetId` doesn't already exist in the user's widget list.
2332    /// This ensures new built-in widgets are available to existing users.
2333    fn merge_default_widgets(&mut self) {
2334        let default_widgets = crate::status_bar::default_widgets();
2335
2336        let existing_ids: std::collections::HashSet<crate::status_bar::WidgetId> = self
2337            .status_bar_widgets
2338            .iter()
2339            .map(|w| w.id.clone())
2340            .collect();
2341
2342        let mut added_count = 0;
2343        for default_widget in default_widgets {
2344            if !existing_ids.contains(&default_widget.id) {
2345                log::info!(
2346                    "Adding new default status bar widget: {:?}",
2347                    default_widget.id
2348                );
2349                self.status_bar_widgets.push(default_widget);
2350                added_count += 1;
2351            }
2352        }
2353
2354        if added_count > 0 {
2355            log::info!(
2356                "Merged {} new default status bar widget(s) into user config",
2357                added_count
2358            );
2359        }
2360    }
2361
2362    /// Generate keybindings for snippets and actions that have keybindings configured.
2363    ///
2364    /// This method adds or updates keybindings for snippets and actions in the keybindings list,
2365    /// using the format "snippet:<id>" for snippets and "action:<id>" for actions.
2366    /// If a keybinding for a snippet/action already exists, it will be updated with the new key.
2367    pub fn generate_snippet_action_keybindings(&mut self) {
2368        use crate::config::KeyBinding;
2369
2370        // Track actions we've seen to remove stale keybindings later
2371        let mut seen_actions = std::collections::HashSet::new();
2372        let mut added_count = 0;
2373        let mut updated_count = 0;
2374
2375        // Generate keybindings for snippets
2376        for snippet in &self.snippets {
2377            if let Some(key) = &snippet.keybinding {
2378                let action = format!("snippet:{}", snippet.id);
2379                seen_actions.insert(action.clone());
2380
2381                if !key.is_empty() && snippet.enabled && snippet.keybinding_enabled {
2382                    // Check if this action already has a keybinding
2383                    if let Some(existing) =
2384                        self.keybindings.iter_mut().find(|kb| kb.action == action)
2385                    {
2386                        // Update existing keybinding if the key changed
2387                        if existing.key != *key {
2388                            log::info!(
2389                                "Updating keybinding for snippet '{}': {} -> {} (was: {})",
2390                                snippet.title,
2391                                key,
2392                                action,
2393                                existing.key
2394                            );
2395                            existing.key = key.clone();
2396                            updated_count += 1;
2397                        }
2398                    } else {
2399                        // Add new keybinding
2400                        log::info!(
2401                            "Adding keybinding for snippet '{}': {} -> {} (enabled={}, keybinding_enabled={})",
2402                            snippet.title,
2403                            key,
2404                            action,
2405                            snippet.enabled,
2406                            snippet.keybinding_enabled
2407                        );
2408                        self.keybindings.push(KeyBinding {
2409                            key: key.clone(),
2410                            action,
2411                        });
2412                        added_count += 1;
2413                    }
2414                } else if !key.is_empty() {
2415                    log::info!(
2416                        "Skipping keybinding for snippet '{}': {} (enabled={}, keybinding_enabled={})",
2417                        snippet.title,
2418                        key,
2419                        snippet.enabled,
2420                        snippet.keybinding_enabled
2421                    );
2422                }
2423            }
2424        }
2425
2426        // Generate keybindings for actions
2427        for action_config in &self.actions {
2428            if let Some(key) = action_config.keybinding() {
2429                let action = format!("action:{}", action_config.id());
2430                seen_actions.insert(action.clone());
2431
2432                if !key.is_empty() && action_config.keybinding_enabled() {
2433                    // Check if this action already has a keybinding
2434                    if let Some(existing) =
2435                        self.keybindings.iter_mut().find(|kb| kb.action == action)
2436                    {
2437                        // Update existing keybinding if the key changed
2438                        if existing.key != key {
2439                            log::info!(
2440                                "Updating keybinding for action '{}': {} -> {} (was: {})",
2441                                action_config.title(),
2442                                key,
2443                                action,
2444                                existing.key
2445                            );
2446                            existing.key = key.to_string();
2447                            updated_count += 1;
2448                        }
2449                    } else {
2450                        // Add new keybinding
2451                        log::info!(
2452                            "Adding keybinding for action '{}': {} -> {} (keybinding_enabled={})",
2453                            action_config.title(),
2454                            key,
2455                            action,
2456                            action_config.keybinding_enabled()
2457                        );
2458                        self.keybindings.push(KeyBinding {
2459                            key: key.to_string(),
2460                            action,
2461                        });
2462                        added_count += 1;
2463                    }
2464                } else if !key.is_empty() {
2465                    log::info!(
2466                        "Skipping keybinding for action '{}': {} (keybinding_enabled={})",
2467                        action_config.title(),
2468                        key,
2469                        action_config.keybinding_enabled()
2470                    );
2471                }
2472            }
2473        }
2474
2475        // Remove stale keybindings for snippets that no longer have keybindings or are disabled
2476        let original_len = self.keybindings.len();
2477        self.keybindings.retain(|kb| {
2478            // Keep if it's not a snippet/action keybinding
2479            if !kb.action.starts_with("snippet:") && !kb.action.starts_with("action:") {
2480                return true;
2481            }
2482            // Keep if we saw it during our scan
2483            seen_actions.contains(&kb.action)
2484        });
2485        let removed_count = original_len - self.keybindings.len();
2486
2487        if added_count > 0 || updated_count > 0 || removed_count > 0 {
2488            log::info!(
2489                "Snippet/Action keybindings: {} added, {} updated, {} removed",
2490                added_count,
2491                updated_count,
2492                removed_count
2493            );
2494        }
2495    }
2496
2497    /// Save configuration to file
2498    pub fn save(&self) -> Result<()> {
2499        let config_path = Self::config_path();
2500
2501        // Create parent directory if it doesn't exist
2502        if let Some(parent) = config_path.parent() {
2503            fs::create_dir_all(parent)?;
2504        }
2505
2506        let yaml = serde_yaml::to_string(self)?;
2507        fs::write(&config_path, yaml)?;
2508
2509        Ok(())
2510    }
2511
2512    /// Get the configuration file path (using XDG convention)
2513    pub fn config_path() -> PathBuf {
2514        #[cfg(target_os = "windows")]
2515        {
2516            if let Some(config_dir) = dirs::config_dir() {
2517                config_dir.join("par-term").join("config.yaml")
2518            } else {
2519                PathBuf::from("config.yaml")
2520            }
2521        }
2522        #[cfg(not(target_os = "windows"))]
2523        {
2524            // Use XDG convention on all platforms: ~/.config/par-term/config.yaml
2525            if let Some(home_dir) = dirs::home_dir() {
2526                home_dir
2527                    .join(".config")
2528                    .join("par-term")
2529                    .join("config.yaml")
2530            } else {
2531                // Fallback if home directory cannot be determined
2532                PathBuf::from("config.yaml")
2533            }
2534        }
2535    }
2536
2537    /// Resolve the tmux executable path at runtime.
2538    /// If the configured path is absolute and exists, use it.
2539    /// If it's "tmux" (the default), search PATH and common installation locations.
2540    /// This handles cases where PATH may be incomplete (e.g., app launched from Finder).
2541    pub fn resolve_tmux_path(&self) -> String {
2542        let configured = &self.tmux_path;
2543
2544        // If it's an absolute path and exists, use it directly
2545        if configured.starts_with('/') && std::path::Path::new(configured).exists() {
2546            return configured.clone();
2547        }
2548
2549        // If it's not just "tmux", return it and let the OS try
2550        if configured != "tmux" {
2551            return configured.clone();
2552        }
2553
2554        // Search for tmux in PATH
2555        if let Ok(path_env) = std::env::var("PATH") {
2556            let separator = if cfg!(windows) { ';' } else { ':' };
2557            let executable = if cfg!(windows) { "tmux.exe" } else { "tmux" };
2558
2559            for dir in path_env.split(separator) {
2560                let candidate = std::path::Path::new(dir).join(executable);
2561                if candidate.exists() {
2562                    return candidate.to_string_lossy().to_string();
2563                }
2564            }
2565        }
2566
2567        // Fall back to common paths for environments where PATH might be incomplete
2568        #[cfg(target_os = "macos")]
2569        {
2570            let macos_paths = [
2571                "/opt/homebrew/bin/tmux", // Homebrew on Apple Silicon
2572                "/usr/local/bin/tmux",    // Homebrew on Intel / MacPorts
2573            ];
2574            for path in macos_paths {
2575                if std::path::Path::new(path).exists() {
2576                    return path.to_string();
2577                }
2578            }
2579        }
2580
2581        #[cfg(target_os = "linux")]
2582        {
2583            let linux_paths = [
2584                "/usr/bin/tmux",       // Most distros
2585                "/usr/local/bin/tmux", // Manual install
2586                "/snap/bin/tmux",      // Snap package
2587            ];
2588            for path in linux_paths {
2589                if std::path::Path::new(path).exists() {
2590                    return path.to_string();
2591                }
2592            }
2593        }
2594
2595        // Final fallback - return configured value
2596        configured.clone()
2597    }
2598
2599    /// Get the session logs directory path, resolving ~ if present
2600    /// Creates the directory if it doesn't exist
2601    pub fn logs_dir(&self) -> PathBuf {
2602        let path = if self.session_log_directory.starts_with("~/") {
2603            if let Some(home) = dirs::home_dir() {
2604                home.join(&self.session_log_directory[2..])
2605            } else {
2606                PathBuf::from(&self.session_log_directory)
2607            }
2608        } else {
2609            PathBuf::from(&self.session_log_directory)
2610        };
2611
2612        // Create directory if it doesn't exist
2613        if !path.exists()
2614            && let Err(e) = std::fs::create_dir_all(&path)
2615        {
2616            log::warn!("Failed to create logs directory {:?}: {}", path, e);
2617        }
2618
2619        path
2620    }
2621
2622    /// Get the shaders directory path (using XDG convention)
2623    pub fn shaders_dir() -> PathBuf {
2624        #[cfg(target_os = "windows")]
2625        {
2626            if let Some(config_dir) = dirs::config_dir() {
2627                config_dir.join("par-term").join("shaders")
2628            } else {
2629                PathBuf::from("shaders")
2630            }
2631        }
2632        #[cfg(not(target_os = "windows"))]
2633        {
2634            if let Some(home_dir) = dirs::home_dir() {
2635                home_dir.join(".config").join("par-term").join("shaders")
2636            } else {
2637                PathBuf::from("shaders")
2638            }
2639        }
2640    }
2641
2642    /// Get the full path to a shader file
2643    /// If the shader path is absolute, returns it as-is
2644    /// Otherwise, resolves it relative to the shaders directory
2645    pub fn shader_path(shader_name: &str) -> PathBuf {
2646        let path = PathBuf::from(shader_name);
2647        if path.is_absolute() {
2648            path
2649        } else {
2650            Self::shaders_dir().join(shader_name)
2651        }
2652    }
2653
2654    /// Resolve a texture path, expanding ~ to home directory
2655    /// and resolving relative paths relative to the shaders directory.
2656    /// Returns the expanded path or the original if expansion fails
2657    pub fn resolve_texture_path(path: &str) -> PathBuf {
2658        if path.starts_with("~/")
2659            && let Some(home) = dirs::home_dir()
2660        {
2661            return home.join(&path[2..]);
2662        }
2663        let path_buf = PathBuf::from(path);
2664        if path_buf.is_absolute() {
2665            path_buf
2666        } else {
2667            Self::shaders_dir().join(path)
2668        }
2669    }
2670
2671    /// Get the channel texture paths as an array of Options
2672    /// Returns [channel0, channel1, channel2, channel3] for iChannel0-3
2673    #[allow(dead_code)]
2674    pub fn shader_channel_paths(&self) -> [Option<PathBuf>; 4] {
2675        [
2676            self.custom_shader_channel0
2677                .as_ref()
2678                .map(|p| Self::resolve_texture_path(p)),
2679            self.custom_shader_channel1
2680                .as_ref()
2681                .map(|p| Self::resolve_texture_path(p)),
2682            self.custom_shader_channel2
2683                .as_ref()
2684                .map(|p| Self::resolve_texture_path(p)),
2685            self.custom_shader_channel3
2686                .as_ref()
2687                .map(|p| Self::resolve_texture_path(p)),
2688        ]
2689    }
2690
2691    /// Get the cubemap path prefix (resolved)
2692    /// Returns None if not configured, otherwise the resolved path prefix
2693    #[allow(dead_code)]
2694    pub fn shader_cubemap_path(&self) -> Option<PathBuf> {
2695        self.custom_shader_cubemap
2696            .as_ref()
2697            .map(|p| Self::resolve_texture_path(p))
2698    }
2699
2700    /// Set the window title
2701    #[allow(dead_code)]
2702    pub fn with_title(mut self, title: impl Into<String>) -> Self {
2703        self.window_title = title.into();
2704        self
2705    }
2706
2707    /// Load theme configuration
2708    pub fn load_theme(&self) -> Theme {
2709        Theme::by_name(&self.theme).unwrap_or_default()
2710    }
2711
2712    /// Apply system theme if auto_dark_mode is enabled.
2713    /// Returns true if the theme was changed.
2714    pub fn apply_system_theme(&mut self, is_dark: bool) -> bool {
2715        if !self.auto_dark_mode {
2716            return false;
2717        }
2718        let new_theme = if is_dark {
2719            &self.dark_theme
2720        } else {
2721            &self.light_theme
2722        };
2723        if self.theme != *new_theme {
2724            self.theme = new_theme.clone();
2725            true
2726        } else {
2727            false
2728        }
2729    }
2730
2731    /// Apply tab style based on system theme when tab_style is Automatic.
2732    /// Returns true if the style was applied.
2733    pub fn apply_system_tab_style(&mut self, is_dark: bool) -> bool {
2734        if self.tab_style != TabStyle::Automatic {
2735            return false;
2736        }
2737        let target = if is_dark {
2738            self.dark_tab_style
2739        } else {
2740            self.light_tab_style
2741        };
2742        // Temporarily set to concrete style, apply colors, then restore Automatic
2743        self.tab_style = target;
2744        self.apply_tab_style();
2745        self.tab_style = TabStyle::Automatic;
2746        true
2747    }
2748
2749    /// Get the user override config for a specific shader (if any)
2750    pub fn get_shader_override(&self, shader_name: &str) -> Option<&ShaderConfig> {
2751        self.shader_configs.get(shader_name)
2752    }
2753
2754    /// Get the user override config for a specific cursor shader (if any)
2755    pub fn get_cursor_shader_override(&self, shader_name: &str) -> Option<&CursorShaderConfig> {
2756        self.cursor_shader_configs.get(shader_name)
2757    }
2758
2759    /// Get or create a mutable reference to a shader's config override
2760    pub fn get_or_create_shader_override(&mut self, shader_name: &str) -> &mut ShaderConfig {
2761        self.shader_configs
2762            .entry(shader_name.to_string())
2763            .or_default()
2764    }
2765
2766    /// Get or create a mutable reference to a cursor shader's config override
2767    pub fn get_or_create_cursor_shader_override(
2768        &mut self,
2769        shader_name: &str,
2770    ) -> &mut CursorShaderConfig {
2771        self.cursor_shader_configs
2772            .entry(shader_name.to_string())
2773            .or_default()
2774    }
2775
2776    /// Remove a shader config override (revert to defaults)
2777    pub fn remove_shader_override(&mut self, shader_name: &str) {
2778        self.shader_configs.remove(shader_name);
2779    }
2780
2781    /// Remove a cursor shader config override (revert to defaults)
2782    pub fn remove_cursor_shader_override(&mut self, shader_name: &str) {
2783        self.cursor_shader_configs.remove(shader_name);
2784    }
2785
2786    /// Check if the shaders folder is missing or empty
2787    /// Returns true if user should be prompted to install shaders
2788    pub fn should_prompt_shader_install(&self) -> bool {
2789        // Only prompt if the preference is set to "ask"
2790        if self.shader_install_prompt != ShaderInstallPrompt::Ask {
2791            return false;
2792        }
2793
2794        let shaders_dir = Self::shaders_dir();
2795
2796        // Check if directory doesn't exist
2797        if !shaders_dir.exists() {
2798            return true;
2799        }
2800
2801        // Check if directory is empty or has no .glsl files
2802        if let Ok(entries) = std::fs::read_dir(&shaders_dir) {
2803            for entry in entries.flatten() {
2804                if let Some(ext) = entry.path().extension()
2805                    && ext == "glsl"
2806                {
2807                    return false; // Found at least one shader
2808                }
2809            }
2810        }
2811
2812        true // Directory exists but has no .glsl files
2813    }
2814
2815    /// Get the configuration directory path (using XDG convention)
2816    pub fn config_dir() -> PathBuf {
2817        #[cfg(target_os = "windows")]
2818        {
2819            if let Some(config_dir) = dirs::config_dir() {
2820                config_dir.join("par-term")
2821            } else {
2822                PathBuf::from(".")
2823            }
2824        }
2825        #[cfg(not(target_os = "windows"))]
2826        {
2827            if let Some(home_dir) = dirs::home_dir() {
2828                home_dir.join(".config").join("par-term")
2829            } else {
2830                PathBuf::from(".")
2831            }
2832        }
2833    }
2834
2835    /// Get the shell integration directory (same as config dir)
2836    pub fn shell_integration_dir() -> PathBuf {
2837        Self::config_dir()
2838    }
2839
2840    /// Check if shell integration should be prompted
2841    pub fn should_prompt_shell_integration(&self) -> bool {
2842        if self.shell_integration_state != InstallPromptState::Ask {
2843            return false;
2844        }
2845
2846        let current_version = env!("CARGO_PKG_VERSION");
2847
2848        // Check if already prompted for this version
2849        if let Some(ref prompted) = self.integration_versions.shell_integration_prompted_version
2850            && prompted == current_version
2851        {
2852            return false;
2853        }
2854
2855        // Check if installed and up to date
2856        if let Some(ref installed) = self
2857            .integration_versions
2858            .shell_integration_installed_version
2859            && installed == current_version
2860        {
2861            return false;
2862        }
2863
2864        true
2865    }
2866
2867    /// Check if shaders should be prompted (version-aware logic)
2868    pub fn should_prompt_shader_install_versioned(&self) -> bool {
2869        if self.shader_install_prompt != ShaderInstallPrompt::Ask {
2870            return false;
2871        }
2872
2873        let current_version = env!("CARGO_PKG_VERSION");
2874
2875        // Check if already prompted for this version
2876        if let Some(ref prompted) = self.integration_versions.shaders_prompted_version
2877            && prompted == current_version
2878        {
2879            return false;
2880        }
2881
2882        // Check if installed and up to date
2883        if let Some(ref installed) = self.integration_versions.shaders_installed_version
2884            && installed == current_version
2885        {
2886            return false;
2887        }
2888
2889        // Also check if shaders folder exists and has files
2890        let shaders_dir = Self::shaders_dir();
2891        !shaders_dir.exists() || !Self::has_shader_files(&shaders_dir)
2892    }
2893
2894    /// Check if a directory contains shader files (.glsl)
2895    fn has_shader_files(dir: &PathBuf) -> bool {
2896        if let Ok(entries) = std::fs::read_dir(dir) {
2897            for entry in entries.flatten() {
2898                if let Some(ext) = entry.path().extension()
2899                    && ext == "glsl"
2900                {
2901                    return true;
2902                }
2903            }
2904        }
2905        false
2906    }
2907
2908    /// Check if either integration should be prompted
2909    pub fn should_prompt_integrations(&self) -> bool {
2910        self.should_prompt_shader_install_versioned() || self.should_prompt_shell_integration()
2911    }
2912
2913    /// Get the effective startup directory based on configuration mode.
2914    ///
2915    /// Priority:
2916    /// 1. Legacy `working_directory` if set (backward compatibility)
2917    /// 2. Based on `startup_directory_mode`:
2918    ///    - Home: Returns user's home directory
2919    ///    - Previous: Returns `last_working_directory` if valid, else home
2920    ///    - Custom: Returns `startup_directory` if set and valid, else home
2921    ///
2922    /// Returns None if the effective directory doesn't exist (caller should fall back to default).
2923    pub fn get_effective_startup_directory(&self) -> Option<String> {
2924        // Legacy working_directory takes precedence for backward compatibility
2925        if let Some(ref wd) = self.working_directory {
2926            let expanded = Self::expand_home_dir(wd);
2927            if std::path::Path::new(&expanded).exists() {
2928                return Some(expanded);
2929            }
2930            log::warn!(
2931                "Configured working_directory '{}' does not exist, using default",
2932                wd
2933            );
2934        }
2935
2936        match self.startup_directory_mode {
2937            StartupDirectoryMode::Home => {
2938                // Return home directory
2939                dirs::home_dir().map(|p| p.to_string_lossy().to_string())
2940            }
2941            StartupDirectoryMode::Previous => {
2942                // Return last working directory if it exists
2943                if let Some(ref last_dir) = self.last_working_directory {
2944                    let expanded = Self::expand_home_dir(last_dir);
2945                    if std::path::Path::new(&expanded).exists() {
2946                        return Some(expanded);
2947                    }
2948                    log::warn!(
2949                        "Previous session directory '{}' no longer exists, using home",
2950                        last_dir
2951                    );
2952                }
2953                // Fall back to home
2954                dirs::home_dir().map(|p| p.to_string_lossy().to_string())
2955            }
2956            StartupDirectoryMode::Custom => {
2957                // Return custom directory if set and exists
2958                if let Some(ref custom_dir) = self.startup_directory {
2959                    let expanded = Self::expand_home_dir(custom_dir);
2960                    if std::path::Path::new(&expanded).exists() {
2961                        return Some(expanded);
2962                    }
2963                    log::warn!(
2964                        "Custom startup directory '{}' does not exist, using home",
2965                        custom_dir
2966                    );
2967                }
2968                // Fall back to home
2969                dirs::home_dir().map(|p| p.to_string_lossy().to_string())
2970            }
2971        }
2972    }
2973
2974    /// Expand ~ to home directory in a path string
2975    fn expand_home_dir(path: &str) -> String {
2976        if let Some(suffix) = path.strip_prefix("~/")
2977            && let Some(home) = dirs::home_dir()
2978        {
2979            return home.join(suffix).to_string_lossy().to_string();
2980        }
2981        path.to_string()
2982    }
2983
2984    /// Get the state file path for storing session state (like last working directory)
2985    pub fn state_file_path() -> PathBuf {
2986        #[cfg(target_os = "windows")]
2987        {
2988            if let Some(data_dir) = dirs::data_local_dir() {
2989                data_dir.join("par-term").join("state.yaml")
2990            } else {
2991                PathBuf::from("state.yaml")
2992            }
2993        }
2994        #[cfg(not(target_os = "windows"))]
2995        {
2996            if let Some(home_dir) = dirs::home_dir() {
2997                home_dir
2998                    .join(".local")
2999                    .join("share")
3000                    .join("par-term")
3001                    .join("state.yaml")
3002            } else {
3003                PathBuf::from("state.yaml")
3004            }
3005        }
3006    }
3007
3008    /// Save the last working directory to state file
3009    pub fn save_last_working_directory(&mut self, directory: &str) -> Result<()> {
3010        self.last_working_directory = Some(directory.to_string());
3011
3012        // Save to state file for persistence across sessions
3013        let state_path = Self::state_file_path();
3014        if let Some(parent) = state_path.parent() {
3015            fs::create_dir_all(parent)?;
3016        }
3017
3018        // Create a minimal state struct for persistence
3019        #[derive(Serialize)]
3020        struct SessionState {
3021            last_working_directory: Option<String>,
3022        }
3023
3024        let state = SessionState {
3025            last_working_directory: Some(directory.to_string()),
3026        };
3027
3028        let yaml = serde_yaml::to_string(&state)?;
3029        fs::write(&state_path, yaml)?;
3030
3031        log::debug!(
3032            "Saved last working directory to {:?}: {}",
3033            state_path,
3034            directory
3035        );
3036        Ok(())
3037    }
3038
3039    /// Get per-pane background config for a given pane index, if configured
3040    /// Returns (image_path, mode, opacity, darken) tuple for easy conversion to runtime type
3041    pub fn get_pane_background(
3042        &self,
3043        index: usize,
3044    ) -> Option<(String, BackgroundImageMode, f32, f32)> {
3045        self.pane_backgrounds
3046            .iter()
3047            .find(|pb| pb.index == index)
3048            .map(|pb| (pb.image.clone(), pb.mode, pb.opacity, pb.darken))
3049    }
3050
3051    /// Load the last working directory from state file
3052    pub fn load_last_working_directory(&mut self) {
3053        let state_path = Self::state_file_path();
3054        if !state_path.exists() {
3055            return;
3056        }
3057
3058        #[derive(Deserialize)]
3059        struct SessionState {
3060            last_working_directory: Option<String>,
3061        }
3062
3063        match fs::read_to_string(&state_path) {
3064            Ok(contents) => {
3065                if let Ok(state) = serde_yaml::from_str::<SessionState>(&contents)
3066                    && let Some(dir) = state.last_working_directory
3067                {
3068                    log::debug!("Loaded last working directory from state file: {}", dir);
3069                    self.last_working_directory = Some(dir);
3070                }
3071            }
3072            Err(e) => {
3073                log::warn!("Failed to read state file {:?}: {}", state_path, e);
3074            }
3075        }
3076    }
3077}