Skip to main content

par_term_config/config/
mod.rs

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