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