Skip to main content

par_term_config/
types.rs

1//! Configuration types and enums.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6// ============================================================================
7// Keybinding Types
8// ============================================================================
9
10/// Keyboard modifier for keybindings.
11///
12/// This enum is exported for potential future use (e.g., custom keybinding UI).
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15#[allow(dead_code)]
16pub enum KeyModifier {
17    /// Control key
18    Ctrl,
19    /// Alt/Option key
20    Alt,
21    /// Shift key
22    Shift,
23    /// Cmd on macOS, Ctrl on other platforms (cross-platform convenience)
24    CmdOrCtrl,
25    /// Always the Cmd/Super/Windows key
26    Super,
27}
28
29/// A keybinding configuration entry
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct KeyBinding {
32    /// Key combination string, e.g., "CmdOrCtrl+Shift+B"
33    pub key: String,
34    /// Action name, e.g., "toggle_background_shader"
35    pub action: String,
36}
37
38/// VSync mode (presentation mode)
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
40#[serde(rename_all = "lowercase")]
41pub enum VsyncMode {
42    /// No VSync - render as fast as possible (lowest latency, highest GPU usage)
43    Immediate,
44    /// Mailbox VSync - cap at monitor refresh rate with triple buffering (balanced)
45    Mailbox,
46    /// FIFO VSync - strict vsync with double buffering (lowest GPU usage, most compatible)
47    #[default]
48    Fifo,
49}
50
51impl VsyncMode {
52    /// Convert to wgpu::PresentMode
53    #[cfg(feature = "wgpu-types")]
54    pub fn to_present_mode(self) -> wgpu::PresentMode {
55        match self {
56            VsyncMode::Immediate => wgpu::PresentMode::Immediate,
57            VsyncMode::Mailbox => wgpu::PresentMode::Mailbox,
58            VsyncMode::Fifo => wgpu::PresentMode::Fifo,
59        }
60    }
61}
62
63/// GPU power preference for adapter selection
64///
65/// Controls which GPU adapter is preferred when multiple GPUs are available
66/// (e.g., integrated Intel GPU vs discrete NVIDIA/AMD GPU).
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
68#[serde(rename_all = "snake_case")]
69pub enum PowerPreference {
70    /// No preference - let the system decide (default)
71    #[default]
72    None,
73    /// Prefer integrated GPU (Intel/AMD iGPU) - saves battery
74    LowPower,
75    /// Prefer discrete GPU (NVIDIA/AMD) - maximum performance
76    HighPerformance,
77}
78
79impl PowerPreference {
80    /// Convert to wgpu::PowerPreference
81    #[cfg(feature = "wgpu-types")]
82    pub fn to_wgpu(self) -> wgpu::PowerPreference {
83        match self {
84            PowerPreference::None => wgpu::PowerPreference::None,
85            PowerPreference::LowPower => wgpu::PowerPreference::LowPower,
86            PowerPreference::HighPerformance => wgpu::PowerPreference::HighPerformance,
87        }
88    }
89
90    /// Display name for UI
91    pub fn display_name(&self) -> &'static str {
92        match self {
93            PowerPreference::None => "None (System Default)",
94            PowerPreference::LowPower => "Low Power (Integrated GPU)",
95            PowerPreference::HighPerformance => "High Performance (Discrete GPU)",
96        }
97    }
98
99    /// All available power preferences for UI iteration
100    pub fn all() -> &'static [PowerPreference] {
101        &[
102            PowerPreference::None,
103            PowerPreference::LowPower,
104            PowerPreference::HighPerformance,
105        ]
106    }
107}
108
109/// Cursor style
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
111#[serde(rename_all = "lowercase")]
112pub enum CursorStyle {
113    /// Block cursor (fills entire cell)
114    #[default]
115    Block,
116    /// Beam cursor (vertical line at cell start)
117    Beam,
118    /// Underline cursor (horizontal line at cell bottom)
119    Underline,
120}
121
122/// Unfocused cursor style - how the cursor appears when window loses focus
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
124#[serde(rename_all = "lowercase")]
125pub enum UnfocusedCursorStyle {
126    /// Show outline-only (hollow) block cursor when unfocused
127    #[default]
128    Hollow,
129    /// Keep same cursor style when unfocused
130    Same,
131    /// Hide cursor completely when unfocused
132    Hidden,
133}
134
135/// Image scaling quality for inline graphics (Sixel, iTerm2, Kitty)
136///
137/// Controls the GPU texture sampling filter used when scaling inline images.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
139#[serde(rename_all = "lowercase")]
140pub enum ImageScalingMode {
141    /// Nearest-neighbor filtering - sharp pixels, good for pixel art
142    Nearest,
143    /// Bilinear filtering - smooth scaling (default)
144    #[default]
145    Linear,
146}
147
148impl ImageScalingMode {
149    /// Display name for UI
150    pub fn display_name(&self) -> &'static str {
151        match self {
152            ImageScalingMode::Nearest => "Nearest (Sharp)",
153            ImageScalingMode::Linear => "Linear (Smooth)",
154        }
155    }
156
157    /// All available modes for UI iteration
158    pub fn all() -> &'static [ImageScalingMode] {
159        &[ImageScalingMode::Nearest, ImageScalingMode::Linear]
160    }
161
162    /// Convert to wgpu FilterMode
163    #[cfg(feature = "wgpu-types")]
164    pub fn to_filter_mode(self) -> wgpu::FilterMode {
165        match self {
166            ImageScalingMode::Nearest => wgpu::FilterMode::Nearest,
167            ImageScalingMode::Linear => wgpu::FilterMode::Linear,
168        }
169    }
170}
171
172/// Background image display mode
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
174#[serde(rename_all = "lowercase")]
175pub enum BackgroundImageMode {
176    /// Scale to fit window while maintaining aspect ratio (may have letterboxing)
177    Fit,
178    /// Scale to fill window while maintaining aspect ratio (may crop edges)
179    Fill,
180    /// Stretch to fill window exactly (ignores aspect ratio)
181    #[default]
182    Stretch,
183    /// Repeat image in a tiled pattern at original size
184    Tile,
185    /// Center image at original size (no scaling)
186    Center,
187}
188
189/// Default save location for downloaded files
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
191#[serde(rename_all = "snake_case")]
192pub enum DownloadSaveLocation {
193    /// Save to ~/Downloads (default)
194    #[default]
195    Downloads,
196    /// Remember and re-use the last directory the user saved to
197    LastUsed,
198    /// Use the shell's current working directory
199    Cwd,
200    /// Use a custom directory path
201    Custom(String),
202}
203
204impl DownloadSaveLocation {
205    /// Get all non-Custom variants for settings UI dropdown
206    pub fn variants() -> &'static [DownloadSaveLocation] {
207        &[
208            DownloadSaveLocation::Downloads,
209            DownloadSaveLocation::LastUsed,
210            DownloadSaveLocation::Cwd,
211        ]
212    }
213
214    /// Display name for settings UI
215    pub fn display_name(&self) -> &str {
216        match self {
217            DownloadSaveLocation::Downloads => "Downloads folder",
218            DownloadSaveLocation::LastUsed => "Last used directory",
219            DownloadSaveLocation::Cwd => "Current working directory",
220            DownloadSaveLocation::Custom(_) => "Custom directory",
221        }
222    }
223}
224
225/// Background source selection
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
227#[serde(rename_all = "lowercase")]
228pub enum BackgroundMode {
229    /// Use theme's default background color
230    #[default]
231    Default,
232    /// Use a custom solid color
233    Color,
234    /// Use a background image
235    Image,
236}
237
238/// Per-pane background image configuration (for config persistence)
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub struct PaneBackgroundConfig {
241    /// Pane index (0-based)
242    pub index: usize,
243    /// Image path
244    pub image: String,
245    /// Display mode
246    #[serde(default)]
247    pub mode: BackgroundImageMode,
248    /// Opacity
249    #[serde(default = "crate::defaults::background_image_opacity")]
250    pub opacity: f32,
251}
252
253/// Tab visual style preset
254///
255/// Controls the cosmetic appearance of tabs (colors, sizes, spacing).
256/// Each preset applies a set of color/size/spacing adjustments to the tab bar.
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
258#[serde(rename_all = "snake_case")]
259pub enum TabStyle {
260    /// Default dark theme styling
261    #[default]
262    Dark,
263    /// Light theme tab styling
264    Light,
265    /// Smaller tabs, more visible terminal content
266    Compact,
267    /// Clean, minimal tab appearance
268    Minimal,
269    /// Enhanced contrast for accessibility
270    HighContrast,
271    /// Automatically switch between light/dark styles based on system theme
272    Automatic,
273}
274
275impl TabStyle {
276    /// Display name for UI
277    pub fn display_name(&self) -> &'static str {
278        match self {
279            TabStyle::Dark => "Dark",
280            TabStyle::Light => "Light",
281            TabStyle::Compact => "Compact",
282            TabStyle::Minimal => "Minimal",
283            TabStyle::HighContrast => "High Contrast",
284            TabStyle::Automatic => "Automatic",
285        }
286    }
287
288    /// All available styles for UI iteration
289    pub fn all() -> &'static [TabStyle] {
290        &[
291            TabStyle::Dark,
292            TabStyle::Light,
293            TabStyle::Compact,
294            TabStyle::Minimal,
295            TabStyle::HighContrast,
296            TabStyle::Automatic,
297        ]
298    }
299
300    /// All concrete styles (excludes Automatic) — for sub-style dropdowns
301    pub fn all_concrete() -> &'static [TabStyle] {
302        &[
303            TabStyle::Dark,
304            TabStyle::Light,
305            TabStyle::Compact,
306            TabStyle::Minimal,
307            TabStyle::HighContrast,
308        ]
309    }
310}
311
312/// Tab bar position
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
314#[serde(rename_all = "snake_case")]
315pub enum TabBarPosition {
316    /// Tab bar at the top of the window (default)
317    #[default]
318    Top,
319    /// Tab bar at the bottom of the window
320    Bottom,
321    /// Tab bar on the left side of the window (vertical layout)
322    Left,
323}
324
325impl TabBarPosition {
326    /// Display name for UI
327    pub fn display_name(&self) -> &'static str {
328        match self {
329            TabBarPosition::Top => "Top",
330            TabBarPosition::Bottom => "Bottom",
331            TabBarPosition::Left => "Left",
332        }
333    }
334
335    /// All available positions for UI iteration
336    pub fn all() -> &'static [TabBarPosition] {
337        &[
338            TabBarPosition::Top,
339            TabBarPosition::Bottom,
340            TabBarPosition::Left,
341        ]
342    }
343
344    /// Returns true if the tab bar is horizontal (top or bottom)
345    pub fn is_horizontal(&self) -> bool {
346        matches!(self, TabBarPosition::Top | TabBarPosition::Bottom)
347    }
348}
349
350/// Tab bar visibility mode
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
352#[serde(rename_all = "snake_case")]
353pub enum TabBarMode {
354    /// Always show tab bar
355    Always,
356    /// Show tab bar only when there are multiple tabs (default)
357    #[default]
358    WhenMultiple,
359    /// Never show tab bar
360    Never,
361}
362
363/// Status bar position
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
365#[serde(rename_all = "lowercase")]
366pub enum StatusBarPosition {
367    /// Status bar at the top of the window
368    Top,
369    /// Status bar at the bottom of the window (default)
370    #[default]
371    Bottom,
372}
373
374/// Window type for different display modes
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
376#[serde(rename_all = "snake_case")]
377pub enum WindowType {
378    /// Normal window (default)
379    #[default]
380    Normal,
381    /// Start in fullscreen mode
382    Fullscreen,
383    /// Edge-anchored window (for dropdown/Quake-style terminals)
384    /// Note: Edge-anchored windows require additional platform-specific support
385    EdgeTop,
386    /// Edge-anchored to bottom of screen
387    EdgeBottom,
388    /// Edge-anchored to left of screen
389    EdgeLeft,
390    /// Edge-anchored to right of screen
391    EdgeRight,
392}
393
394impl WindowType {
395    /// Display name for UI
396    pub fn display_name(&self) -> &'static str {
397        match self {
398            WindowType::Normal => "Normal",
399            WindowType::Fullscreen => "Fullscreen",
400            WindowType::EdgeTop => "Edge (Top)",
401            WindowType::EdgeBottom => "Edge (Bottom)",
402            WindowType::EdgeLeft => "Edge (Left)",
403            WindowType::EdgeRight => "Edge (Right)",
404        }
405    }
406
407    /// All available window types for UI iteration
408    pub fn all() -> &'static [WindowType] {
409        &[
410            WindowType::Normal,
411            WindowType::Fullscreen,
412            WindowType::EdgeTop,
413            WindowType::EdgeBottom,
414            WindowType::EdgeLeft,
415            WindowType::EdgeRight,
416        ]
417    }
418
419    /// Returns true if this is an edge-anchored window type
420    pub fn is_edge(&self) -> bool {
421        matches!(
422            self,
423            WindowType::EdgeTop
424                | WindowType::EdgeBottom
425                | WindowType::EdgeLeft
426                | WindowType::EdgeRight
427        )
428    }
429}
430
431/// Quote style for dropped file paths
432///
433/// Controls how filenames containing special characters are quoted when dropped into the terminal.
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
435#[serde(rename_all = "snake_case")]
436pub enum DroppedFileQuoteStyle {
437    /// Single quotes - safest for most shells (handles $, !, spaces, etc.)
438    /// Example: '/path/to/file with spaces.txt'
439    #[default]
440    SingleQuotes,
441    /// Double quotes - allows variable expansion
442    /// Example: "/path/to/file with spaces.txt"
443    DoubleQuotes,
444    /// Backslash escaping - escape individual special characters
445    /// Example: /path/to/file\ with\ spaces.txt
446    Backslash,
447    /// No quoting - insert path as-is (not recommended for paths with special chars)
448    None,
449}
450
451impl DroppedFileQuoteStyle {
452    /// Display name for UI
453    pub fn display_name(&self) -> &'static str {
454        match self {
455            DroppedFileQuoteStyle::SingleQuotes => "Single quotes ('...')",
456            DroppedFileQuoteStyle::DoubleQuotes => "Double quotes (\"...\")",
457            DroppedFileQuoteStyle::Backslash => "Backslash escaping (\\)",
458            DroppedFileQuoteStyle::None => "None (raw path)",
459        }
460    }
461
462    /// All available quote styles for UI iteration
463    pub fn all() -> &'static [DroppedFileQuoteStyle] {
464        &[
465            DroppedFileQuoteStyle::SingleQuotes,
466            DroppedFileQuoteStyle::DoubleQuotes,
467            DroppedFileQuoteStyle::Backslash,
468            DroppedFileQuoteStyle::None,
469        ]
470    }
471}
472
473/// Option/Alt key behavior mode
474///
475/// Controls what happens when Option (macOS) or Alt (Linux/Windows) key is pressed
476/// with a character key. This is essential for emacs and vim users who rely on
477/// Meta key combinations (M-x, M-f, M-b, etc.).
478#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
479#[serde(rename_all = "lowercase")]
480pub enum OptionKeyMode {
481    /// Normal - sends special characters (default macOS behavior)
482    /// Option+f → ƒ (special character)
483    Normal,
484    /// Meta - sets the high bit (8th bit) on the character
485    /// Option+f → 0xE6 (f with high bit set)
486    Meta,
487    /// Esc - sends Escape prefix before the character (most compatible)
488    /// Option+f → ESC f (escape then f)
489    /// This is the most compatible mode for terminal applications like emacs and vim
490    #[default]
491    Esc,
492}
493
494// ============================================================================
495// Modifier Remapping Types
496// ============================================================================
497
498/// Target modifier for remapping.
499///
500/// Allows remapping one modifier key to behave as another.
501/// For example, remap Caps Lock to Ctrl, or swap Ctrl and Super.
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
503#[serde(rename_all = "lowercase")]
504pub enum ModifierTarget {
505    /// No remapping - use the key's normal function
506    #[default]
507    None,
508    /// Remap to Control key
509    Ctrl,
510    /// Remap to Alt/Option key
511    Alt,
512    /// Remap to Shift key
513    Shift,
514    /// Remap to Super/Cmd/Windows key
515    Super,
516}
517
518impl ModifierTarget {
519    /// Display name for UI
520    pub fn display_name(&self) -> &'static str {
521        match self {
522            ModifierTarget::None => "None (disabled)",
523            ModifierTarget::Ctrl => "Ctrl",
524            ModifierTarget::Alt => "Alt/Option",
525            ModifierTarget::Shift => "Shift",
526            ModifierTarget::Super => "Super/Cmd",
527        }
528    }
529
530    /// All available targets for UI iteration
531    pub fn all() -> &'static [ModifierTarget] {
532        &[
533            ModifierTarget::None,
534            ModifierTarget::Ctrl,
535            ModifierTarget::Alt,
536            ModifierTarget::Shift,
537            ModifierTarget::Super,
538        ]
539    }
540}
541
542/// Modifier remapping configuration.
543///
544/// Allows users to remap modifier keys to different functions.
545/// This is useful for:
546/// - Swapping Ctrl and Caps Lock
547/// - Using Ctrl as Cmd on macOS
548/// - Customizing modifier layout for ergonomic keyboards
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
550pub struct ModifierRemapping {
551    /// What the left Ctrl key should act as
552    #[serde(default)]
553    pub left_ctrl: ModifierTarget,
554    /// What the right Ctrl key should act as
555    #[serde(default)]
556    pub right_ctrl: ModifierTarget,
557    /// What the left Alt key should act as
558    #[serde(default)]
559    pub left_alt: ModifierTarget,
560    /// What the right Alt key should act as
561    #[serde(default)]
562    pub right_alt: ModifierTarget,
563    /// What the left Super/Cmd key should act as
564    #[serde(default)]
565    pub left_super: ModifierTarget,
566    /// What the right Super/Cmd key should act as
567    #[serde(default)]
568    pub right_super: ModifierTarget,
569}
570
571/// Font mapping for a specific Unicode range
572#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct FontRange {
574    /// Start of Unicode range (inclusive), e.g., 0x4E00 for CJK
575    pub start: u32,
576    /// End of Unicode range (inclusive), e.g., 0x9FFF for CJK
577    pub end: u32,
578    /// Font family name to use for this range
579    pub font_family: String,
580}
581
582/// Thin strokes / font smoothing mode
583///
584/// Controls font stroke weight adjustment for improved rendering,
585/// particularly on high-DPI/Retina displays. Inspired by iTerm2's thin strokes feature.
586#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
587#[serde(rename_all = "snake_case")]
588pub enum ThinStrokesMode {
589    /// Never apply thin strokes
590    Never,
591    /// Apply thin strokes only on Retina/HiDPI displays (default)
592    #[default]
593    RetinaOnly,
594    /// Apply thin strokes only on dark backgrounds
595    DarkBackgroundsOnly,
596    /// Apply thin strokes only on Retina displays with dark backgrounds
597    RetinaDarkBackgroundsOnly,
598    /// Always apply thin strokes
599    Always,
600}
601
602/// Shader install prompt mode
603///
604/// Controls whether the user is prompted to install shaders when the shaders
605/// folder is missing or empty on startup.
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
607#[serde(rename_all = "snake_case")]
608pub enum ShaderInstallPrompt {
609    /// Ask the user if they want to install shaders (default)
610    #[default]
611    Ask,
612    /// Never ask - user declined installation
613    Never,
614    /// Shaders have been installed
615    Installed,
616}
617
618/// State of an integration's install prompt
619#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
620#[serde(rename_all = "snake_case")]
621pub enum InstallPromptState {
622    /// Prompt user when appropriate (default)
623    #[default]
624    Ask,
625    /// User said "never ask again"
626    Never,
627    /// Currently installed
628    Installed,
629}
630
631impl InstallPromptState {
632    /// Display name for UI
633    pub fn display_name(&self) -> &'static str {
634        match self {
635            Self::Ask => "Ask",
636            Self::Never => "Never",
637            Self::Installed => "Installed",
638        }
639    }
640}
641
642/// Tracks installed and prompted versions for integrations
643#[derive(Debug, Clone, Default, Serialize, Deserialize)]
644pub struct IntegrationVersions {
645    /// Version when shaders were installed
646    pub shaders_installed_version: Option<String>,
647    /// Version when user was last prompted about shaders
648    pub shaders_prompted_version: Option<String>,
649    /// Version when shell integration was installed
650    pub shell_integration_installed_version: Option<String>,
651    /// Version when user was last prompted about shell integration
652    pub shell_integration_prompted_version: Option<String>,
653}
654
655/// Startup directory mode
656///
657/// Controls where the terminal starts its working directory.
658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
659#[serde(rename_all = "snake_case")]
660pub enum StartupDirectoryMode {
661    /// Start in the user's home directory (default)
662    #[default]
663    Home,
664    /// Remember and restore the last working directory from the previous session
665    Previous,
666    /// Start in a user-specified custom path
667    Custom,
668}
669
670impl StartupDirectoryMode {
671    /// Display name for UI
672    pub fn display_name(&self) -> &'static str {
673        match self {
674            Self::Home => "Home Directory",
675            Self::Previous => "Previous Session",
676            Self::Custom => "Custom Directory",
677        }
678    }
679
680    /// All available modes for UI iteration
681    pub fn all() -> &'static [StartupDirectoryMode] {
682        &[
683            StartupDirectoryMode::Home,
684            StartupDirectoryMode::Previous,
685            StartupDirectoryMode::Custom,
686        ]
687    }
688}
689
690/// Action to take when the shell process exits
691///
692/// Controls what happens when a shell session terminates.
693#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
694#[serde(rename_all = "snake_case")]
695pub enum ShellExitAction {
696    /// Close the tab/pane when shell exits (default)
697    #[default]
698    Close,
699    /// Keep the pane open showing the terminated shell
700    Keep,
701    /// Immediately restart the shell
702    RestartImmediately,
703    /// Show a prompt message and wait for Enter before restarting
704    RestartWithPrompt,
705    /// Restart the shell after a 1 second delay
706    RestartAfterDelay,
707}
708
709impl ShellExitAction {
710    /// Display name for UI
711    pub fn display_name(&self) -> &'static str {
712        match self {
713            Self::Close => "Close tab/pane",
714            Self::Keep => "Keep open",
715            Self::RestartImmediately => "Restart immediately",
716            Self::RestartWithPrompt => "Restart with prompt",
717            Self::RestartAfterDelay => "Restart after 1s delay",
718        }
719    }
720
721    /// All available actions for UI iteration
722    pub fn all() -> &'static [ShellExitAction] {
723        &[
724            ShellExitAction::Close,
725            ShellExitAction::Keep,
726            ShellExitAction::RestartImmediately,
727            ShellExitAction::RestartWithPrompt,
728            ShellExitAction::RestartAfterDelay,
729        ]
730    }
731
732    /// Returns true if this action involves restarting the shell
733    pub fn is_restart(&self) -> bool {
734        matches!(
735            self,
736            Self::RestartImmediately | Self::RestartWithPrompt | Self::RestartAfterDelay
737        )
738    }
739}
740
741/// Detected shell type
742#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
743#[serde(rename_all = "lowercase")]
744pub enum ShellType {
745    Bash,
746    Zsh,
747    Fish,
748    #[default]
749    Unknown,
750}
751
752impl ShellType {
753    /// Classify a shell path string into a `ShellType`.
754    fn from_path(path: &str) -> Self {
755        if path.contains("zsh") {
756            Self::Zsh
757        } else if path.contains("bash") {
758            Self::Bash
759        } else if path.contains("fish") {
760            Self::Fish
761        } else {
762            Self::Unknown
763        }
764    }
765
766    /// Detect shell type using multiple strategies.
767    ///
768    /// 1. `$SHELL` environment variable (works in terminals).
769    /// 2. macOS Directory Services (`dscl`) — works for app-bundle launches.
770    /// 3. `/etc/passwd` entry for the current user (Linux / older macOS).
771    pub fn detect() -> Self {
772        // 1. $SHELL env var
773        if let Ok(shell) = std::env::var("SHELL") {
774            let t = Self::from_path(&shell);
775            if t != Self::Unknown {
776                return t;
777            }
778        }
779
780        // 2. macOS: query Directory Services for the login shell
781        #[cfg(target_os = "macos")]
782        {
783            if let Some(t) = Self::detect_via_dscl() {
784                return t;
785            }
786        }
787
788        // 3. Parse /etc/passwd for the current user's shell
789        #[cfg(unix)]
790        {
791            if let Some(t) = Self::detect_from_passwd() {
792                return t;
793            }
794        }
795
796        Self::Unknown
797    }
798
799    /// macOS: run `dscl . -read /Users/<user> UserShell` to get the login shell.
800    #[cfg(target_os = "macos")]
801    fn detect_via_dscl() -> Option<Self> {
802        let user = std::env::var("USER")
803            .or_else(|_| std::env::var("LOGNAME"))
804            .ok()?;
805        let output = std::process::Command::new("dscl")
806            .args([".", "-read", &format!("/Users/{}", user), "UserShell"])
807            .output()
808            .ok()?;
809        let text = String::from_utf8_lossy(&output.stdout);
810        // Output looks like: "UserShell: /bin/zsh"
811        let shell_path = text.split_whitespace().last()?;
812        let t = Self::from_path(shell_path);
813        if t != Self::Unknown { Some(t) } else { None }
814    }
815
816    /// Unix: parse `/etc/passwd` for the current user's configured shell.
817    #[cfg(unix)]
818    fn detect_from_passwd() -> Option<Self> {
819        let user = std::env::var("USER")
820            .or_else(|_| std::env::var("LOGNAME"))
821            .ok()?;
822        let contents = std::fs::read_to_string("/etc/passwd").ok()?;
823        for line in contents.lines() {
824            let parts: Vec<&str> = line.splitn(7, ':').collect();
825            if parts.len() == 7 && parts[0] == user {
826                let t = Self::from_path(parts[6]);
827                if t != Self::Unknown {
828                    return Some(t);
829                }
830            }
831        }
832        None
833    }
834
835    /// Display name for UI
836    pub fn display_name(&self) -> &'static str {
837        match self {
838            Self::Bash => "Bash",
839            Self::Zsh => "Zsh",
840            Self::Fish => "Fish",
841            Self::Unknown => "Unknown",
842        }
843    }
844
845    /// File extension for integration script
846    pub fn extension(&self) -> &'static str {
847        match self {
848            Self::Bash => "bash",
849            Self::Zsh => "zsh",
850            Self::Fish => "fish",
851            Self::Unknown => "sh",
852        }
853    }
854}
855
856/// Update check frequency
857///
858/// Controls how often par-term checks GitHub for new releases.
859#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
860#[serde(rename_all = "snake_case")]
861pub enum UpdateCheckFrequency {
862    /// Never check for updates
863    Never,
864    /// Check once per day (default)
865    #[default]
866    Daily,
867    /// Check once per week
868    Weekly,
869    /// Check once per month
870    Monthly,
871}
872
873impl UpdateCheckFrequency {
874    /// Get the duration in seconds for this frequency
875    pub fn as_seconds(&self) -> Option<u64> {
876        match self {
877            UpdateCheckFrequency::Never => None,
878            UpdateCheckFrequency::Daily => Some(24 * 60 * 60), // 86400
879            UpdateCheckFrequency::Weekly => Some(7 * 24 * 60 * 60), // 604800
880            UpdateCheckFrequency::Monthly => Some(30 * 24 * 60 * 60), // 2592000
881        }
882    }
883
884    /// Display name for UI
885    pub fn display_name(&self) -> &'static str {
886        match self {
887            UpdateCheckFrequency::Never => "Never",
888            UpdateCheckFrequency::Daily => "Daily",
889            UpdateCheckFrequency::Weekly => "Weekly",
890            UpdateCheckFrequency::Monthly => "Monthly",
891        }
892    }
893}
894
895// ============================================================================
896// Session Logging Types
897// ============================================================================
898
899/// Log format for session logging
900///
901/// Controls the format used when automatically logging terminal sessions.
902#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
903#[serde(rename_all = "lowercase")]
904pub enum SessionLogFormat {
905    /// Plain text - strips escape sequences, captures only printable output
906    Plain,
907    /// HTML - preserves colors and styling as HTML
908    Html,
909    /// Asciicast v2 - asciinema-compatible format for replay/sharing
910    #[default]
911    Asciicast,
912}
913
914impl SessionLogFormat {
915    /// Display name for UI
916    pub fn display_name(&self) -> &'static str {
917        match self {
918            SessionLogFormat::Plain => "Plain Text",
919            SessionLogFormat::Html => "HTML",
920            SessionLogFormat::Asciicast => "Asciicast (asciinema)",
921        }
922    }
923
924    /// All available formats for UI iteration
925    pub fn all() -> &'static [SessionLogFormat] {
926        &[
927            SessionLogFormat::Plain,
928            SessionLogFormat::Html,
929            SessionLogFormat::Asciicast,
930        ]
931    }
932
933    /// File extension for this format
934    pub fn extension(&self) -> &'static str {
935        match self {
936            SessionLogFormat::Plain => "txt",
937            SessionLogFormat::Html => "html",
938            SessionLogFormat::Asciicast => "cast",
939        }
940    }
941}
942
943/// Log level for debug logging to file.
944///
945/// Controls the verbosity of log output written to the debug log file.
946/// Environment variables `RUST_LOG` and `--log-level` CLI flag take precedence.
947#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
948#[serde(rename_all = "lowercase")]
949pub enum LogLevel {
950    /// No logging (log file not created)
951    #[default]
952    Off,
953    /// Errors only
954    Error,
955    /// Warnings and errors
956    Warn,
957    /// Informational messages
958    Info,
959    /// Debug messages
960    Debug,
961    /// Most verbose
962    Trace,
963}
964
965impl LogLevel {
966    /// Display name for UI
967    pub fn display_name(&self) -> &'static str {
968        match self {
969            LogLevel::Off => "Off",
970            LogLevel::Error => "Error",
971            LogLevel::Warn => "Warn",
972            LogLevel::Info => "Info",
973            LogLevel::Debug => "Debug",
974            LogLevel::Trace => "Trace",
975        }
976    }
977
978    /// All available levels for UI iteration
979    pub fn all() -> &'static [LogLevel] {
980        &[
981            LogLevel::Off,
982            LogLevel::Error,
983            LogLevel::Warn,
984            LogLevel::Info,
985            LogLevel::Debug,
986            LogLevel::Trace,
987        ]
988    }
989
990    /// Convert to `log::LevelFilter`
991    pub fn to_level_filter(self) -> log::LevelFilter {
992        match self {
993            LogLevel::Off => log::LevelFilter::Off,
994            LogLevel::Error => log::LevelFilter::Error,
995            LogLevel::Warn => log::LevelFilter::Warn,
996            LogLevel::Info => log::LevelFilter::Info,
997            LogLevel::Debug => log::LevelFilter::Debug,
998            LogLevel::Trace => log::LevelFilter::Trace,
999        }
1000    }
1001}
1002
1003/// Editor selection mode for semantic history
1004///
1005/// Controls how the editor is selected when opening files via semantic history.
1006#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1007#[serde(rename_all = "snake_case")]
1008pub enum SemanticHistoryEditorMode {
1009    /// Use custom editor command from `semantic_history_editor` setting
1010    Custom,
1011    /// Use $EDITOR or $VISUAL environment variable
1012    #[default]
1013    EnvironmentVariable,
1014    /// Use system default application for each file type
1015    SystemDefault,
1016}
1017
1018impl SemanticHistoryEditorMode {
1019    /// Display name for UI
1020    pub fn display_name(&self) -> &'static str {
1021        match self {
1022            SemanticHistoryEditorMode::Custom => "Custom Editor",
1023            SemanticHistoryEditorMode::EnvironmentVariable => "Environment Variable ($EDITOR)",
1024            SemanticHistoryEditorMode::SystemDefault => "System Default",
1025        }
1026    }
1027
1028    /// All available modes for UI iteration
1029    pub fn all() -> &'static [SemanticHistoryEditorMode] {
1030        &[
1031            SemanticHistoryEditorMode::Custom,
1032            SemanticHistoryEditorMode::EnvironmentVariable,
1033            SemanticHistoryEditorMode::SystemDefault,
1034        ]
1035    }
1036}
1037
1038// ============================================================================
1039// Per-Shader Configuration Types
1040// ============================================================================
1041
1042/// Metadata embedded in shader files via YAML block comments.
1043///
1044/// Parsed from `/*! par-term shader metadata ... */` blocks at the top of shader files.
1045#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1046pub struct ShaderMetadata {
1047    /// Human-readable name for the shader (e.g., "CRT Effect")
1048    pub name: Option<String>,
1049    /// Author of the shader
1050    pub author: Option<String>,
1051    /// Description of what the shader does
1052    pub description: Option<String>,
1053    /// Version string (e.g., "1.0.0")
1054    pub version: Option<String>,
1055    /// Default configuration values for this shader
1056    #[serde(default)]
1057    pub defaults: ShaderConfig,
1058}
1059
1060/// Per-shader configuration settings.
1061///
1062/// Used both for embedded defaults in shader files and for user overrides in config.yaml.
1063/// All fields are optional to allow partial overrides.
1064#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1065pub struct ShaderConfig {
1066    /// Animation speed multiplier (1.0 = normal speed)
1067    pub animation_speed: Option<f32>,
1068    /// Brightness multiplier (0.05-1.0)
1069    pub brightness: Option<f32>,
1070    /// Text opacity when using this shader (0.0-1.0)
1071    pub text_opacity: Option<f32>,
1072    /// When true, shader receives full terminal content for manipulation
1073    pub full_content: Option<bool>,
1074    /// Path to texture for iChannel0
1075    pub channel0: Option<String>,
1076    /// Path to texture for iChannel1
1077    pub channel1: Option<String>,
1078    /// Path to texture for iChannel2
1079    pub channel2: Option<String>,
1080    /// Path to texture for iChannel3
1081    pub channel3: Option<String>,
1082    /// Path prefix for cubemap faces
1083    pub cubemap: Option<String>,
1084    /// Whether cubemap sampling is enabled
1085    pub cubemap_enabled: Option<bool>,
1086    /// Use the app's background image as iChannel0 instead of a separate texture
1087    pub use_background_as_channel0: Option<bool>,
1088}
1089
1090/// Cursor shader specific configuration.
1091///
1092/// Extends base ShaderConfig with cursor-specific settings.
1093#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1094pub struct CursorShaderConfig {
1095    /// Base shader configuration
1096    #[serde(flatten)]
1097    pub base: ShaderConfig,
1098    /// Hide the default cursor when this shader is enabled
1099    pub hides_cursor: Option<bool>,
1100    /// Disable cursor shader while in alt screen (vim, less, htop)
1101    pub disable_in_alt_screen: Option<bool>,
1102    /// Cursor glow radius in pixels
1103    pub glow_radius: Option<f32>,
1104    /// Cursor glow intensity (0.0-1.0)
1105    pub glow_intensity: Option<f32>,
1106    /// Duration of cursor trail effect in seconds
1107    pub trail_duration: Option<f32>,
1108    /// Cursor color for shader effects [R, G, B] (0-255)
1109    pub cursor_color: Option<[u8; 3]>,
1110}
1111
1112/// Metadata embedded in cursor shader files via YAML block comments.
1113///
1114/// Parsed from `/*! par-term shader metadata ... */` blocks at the top of cursor shader files.
1115/// Similar to `ShaderMetadata` but with cursor-specific defaults.
1116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1117pub struct CursorShaderMetadata {
1118    /// Human-readable name for the shader (e.g., "Cursor Glow Effect")
1119    pub name: Option<String>,
1120    /// Author of the shader
1121    pub author: Option<String>,
1122    /// Description of what the shader does
1123    pub description: Option<String>,
1124    /// Version string (e.g., "1.0.0")
1125    pub version: Option<String>,
1126    /// Default configuration values for this cursor shader
1127    #[serde(default)]
1128    pub defaults: CursorShaderConfig,
1129}
1130
1131/// Fully resolved shader configuration with all values filled in.
1132///
1133/// Created by merging user overrides, shader metadata defaults, and global defaults.
1134#[derive(Debug, Clone)]
1135#[allow(dead_code)]
1136pub struct ResolvedShaderConfig {
1137    /// Animation speed multiplier
1138    pub animation_speed: f32,
1139    /// Brightness multiplier
1140    pub brightness: f32,
1141    /// Text opacity
1142    pub text_opacity: f32,
1143    /// Full content mode enabled
1144    pub full_content: bool,
1145    /// Resolved path to iChannel0 texture
1146    pub channel0: Option<PathBuf>,
1147    /// Resolved path to iChannel1 texture
1148    pub channel1: Option<PathBuf>,
1149    /// Resolved path to iChannel2 texture
1150    pub channel2: Option<PathBuf>,
1151    /// Resolved path to iChannel3 texture
1152    pub channel3: Option<PathBuf>,
1153    /// Resolved cubemap path prefix
1154    pub cubemap: Option<PathBuf>,
1155    /// Cubemap sampling enabled
1156    pub cubemap_enabled: bool,
1157    /// Use the app's background image as iChannel0
1158    pub use_background_as_channel0: bool,
1159}
1160
1161impl Default for ResolvedShaderConfig {
1162    fn default() -> Self {
1163        Self {
1164            animation_speed: 1.0,
1165            brightness: 1.0,
1166            text_opacity: 1.0,
1167            full_content: false,
1168            channel0: None,
1169            channel1: None,
1170            channel2: None,
1171            channel3: None,
1172            cubemap: None,
1173            cubemap_enabled: true,
1174            use_background_as_channel0: false,
1175        }
1176    }
1177}
1178
1179/// Fully resolved cursor shader configuration with all values filled in.
1180#[derive(Debug, Clone)]
1181#[allow(dead_code)]
1182pub struct ResolvedCursorShaderConfig {
1183    /// Base resolved shader config
1184    pub base: ResolvedShaderConfig,
1185    /// Hide the default cursor when this shader is enabled
1186    pub hides_cursor: bool,
1187    /// Disable cursor shader while in alt screen (vim, less, htop)
1188    pub disable_in_alt_screen: bool,
1189    /// Cursor glow radius in pixels
1190    pub glow_radius: f32,
1191    /// Cursor glow intensity (0.0-1.0)
1192    pub glow_intensity: f32,
1193    /// Duration of cursor trail effect in seconds
1194    pub trail_duration: f32,
1195    /// Cursor color for shader effects [R, G, B] (0-255)
1196    pub cursor_color: [u8; 3],
1197}
1198
1199impl Default for ResolvedCursorShaderConfig {
1200    fn default() -> Self {
1201        Self {
1202            base: ResolvedShaderConfig::default(),
1203            hides_cursor: false,
1204            disable_in_alt_screen: true,
1205            glow_radius: 80.0,
1206            glow_intensity: 0.3,
1207            trail_duration: 0.5,
1208            cursor_color: [255, 255, 255],
1209        }
1210    }
1211}
1212
1213// ============================================================================
1214// Progress Bar Types
1215// ============================================================================
1216
1217/// Progress bar visual style
1218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1219#[serde(rename_all = "lowercase")]
1220pub enum ProgressBarStyle {
1221    /// Thin bar line (default)
1222    #[default]
1223    Bar,
1224    /// Bar with percentage text
1225    BarWithText,
1226}
1227
1228impl ProgressBarStyle {
1229    /// Display name for UI
1230    pub fn display_name(&self) -> &'static str {
1231        match self {
1232            ProgressBarStyle::Bar => "Bar",
1233            ProgressBarStyle::BarWithText => "Bar with Text",
1234        }
1235    }
1236
1237    /// All available styles for UI iteration
1238    pub fn all() -> &'static [ProgressBarStyle] {
1239        &[ProgressBarStyle::Bar, ProgressBarStyle::BarWithText]
1240    }
1241}
1242
1243/// Progress bar position on screen
1244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1245#[serde(rename_all = "lowercase")]
1246pub enum ProgressBarPosition {
1247    /// Top of the terminal window (default)
1248    #[default]
1249    Top,
1250    /// Bottom of the terminal window
1251    Bottom,
1252}
1253
1254impl ProgressBarPosition {
1255    /// Display name for UI
1256    pub fn display_name(&self) -> &'static str {
1257        match self {
1258            ProgressBarPosition::Bottom => "Bottom",
1259            ProgressBarPosition::Top => "Top",
1260        }
1261    }
1262
1263    /// All available positions for UI iteration
1264    pub fn all() -> &'static [ProgressBarPosition] {
1265        &[ProgressBarPosition::Top, ProgressBarPosition::Bottom]
1266    }
1267}
1268
1269// ============================================================================
1270// Smart Selection Types
1271// ============================================================================
1272
1273/// Precision level for smart selection rules.
1274///
1275/// Higher precision rules are checked first and match more specific patterns.
1276/// Based on iTerm2's smart selection precision levels.
1277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1278#[serde(rename_all = "snake_case")]
1279pub enum SmartSelectionPrecision {
1280    /// Very low precision (0.00001) - matches almost anything
1281    VeryLow,
1282    /// Low precision (0.001) - broad patterns
1283    Low,
1284    /// Normal precision (1.0) - standard patterns
1285    #[default]
1286    Normal,
1287    /// High precision (1000.0) - specific patterns
1288    High,
1289    /// Very high precision (1000000.0) - most specific patterns (checked first)
1290    VeryHigh,
1291}
1292
1293impl SmartSelectionPrecision {
1294    /// Get the numeric precision value for sorting
1295    pub fn value(&self) -> f64 {
1296        match self {
1297            SmartSelectionPrecision::VeryLow => 0.00001,
1298            SmartSelectionPrecision::Low => 0.001,
1299            SmartSelectionPrecision::Normal => 1.0,
1300            SmartSelectionPrecision::High => 1000.0,
1301            SmartSelectionPrecision::VeryHigh => 1_000_000.0,
1302        }
1303    }
1304
1305    /// Display name for UI
1306    pub fn display_name(&self) -> &'static str {
1307        match self {
1308            SmartSelectionPrecision::VeryLow => "Very Low",
1309            SmartSelectionPrecision::Low => "Low",
1310            SmartSelectionPrecision::Normal => "Normal",
1311            SmartSelectionPrecision::High => "High",
1312            SmartSelectionPrecision::VeryHigh => "Very High",
1313        }
1314    }
1315}
1316
1317/// Position of pane title bars
1318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1319#[serde(rename_all = "snake_case")]
1320pub enum PaneTitlePosition {
1321    /// Title bar at the top of the pane (default)
1322    #[default]
1323    Top,
1324    /// Title bar at the bottom of the pane
1325    Bottom,
1326}
1327
1328impl PaneTitlePosition {
1329    /// All available positions for UI dropdowns
1330    pub const ALL: &'static [PaneTitlePosition] =
1331        &[PaneTitlePosition::Top, PaneTitlePosition::Bottom];
1332
1333    /// Display name for UI
1334    pub fn display_name(&self) -> &'static str {
1335        match self {
1336            PaneTitlePosition::Top => "Top",
1337            PaneTitlePosition::Bottom => "Bottom",
1338        }
1339    }
1340}
1341
1342/// Style of dividers between panes
1343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1344#[serde(rename_all = "snake_case")]
1345pub enum DividerStyle {
1346    /// Solid line (default)
1347    #[default]
1348    Solid,
1349    /// Double line effect (two thin lines with gap)
1350    Double,
1351    /// Dashed line effect
1352    Dashed,
1353    /// Shadow effect (gradient fade)
1354    Shadow,
1355}
1356
1357impl DividerStyle {
1358    /// All available styles for UI dropdowns
1359    pub const ALL: &'static [DividerStyle] = &[
1360        DividerStyle::Solid,
1361        DividerStyle::Double,
1362        DividerStyle::Dashed,
1363        DividerStyle::Shadow,
1364    ];
1365
1366    /// Display name for UI
1367    pub fn display_name(&self) -> &'static str {
1368        match self {
1369            DividerStyle::Solid => "Solid",
1370            DividerStyle::Double => "Double",
1371            DividerStyle::Dashed => "Dashed",
1372            DividerStyle::Shadow => "Shadow",
1373        }
1374    }
1375}
1376
1377/// Terminal events that can trigger alert sounds
1378#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1379#[serde(rename_all = "snake_case")]
1380pub enum AlertEvent {
1381    /// Bell character received (BEL / 0x07)
1382    Bell,
1383    /// Command completed (requires shell integration)
1384    CommandComplete,
1385    /// A new tab was created
1386    NewTab,
1387    /// A tab was closed
1388    TabClose,
1389}
1390
1391impl AlertEvent {
1392    /// Display name for UI
1393    pub fn display_name(&self) -> &'static str {
1394        match self {
1395            AlertEvent::Bell => "Bell",
1396            AlertEvent::CommandComplete => "Command Complete",
1397            AlertEvent::NewTab => "New Tab",
1398            AlertEvent::TabClose => "Tab Close",
1399        }
1400    }
1401
1402    /// All available events for UI iteration
1403    pub fn all() -> &'static [AlertEvent] {
1404        &[
1405            AlertEvent::Bell,
1406            AlertEvent::CommandComplete,
1407            AlertEvent::NewTab,
1408            AlertEvent::TabClose,
1409        ]
1410    }
1411}
1412
1413/// Configuration for an alert sound tied to a specific event
1414#[derive(Debug, Clone, Serialize, Deserialize)]
1415pub struct AlertSoundConfig {
1416    /// Whether this alert sound is enabled
1417    #[serde(default = "crate::defaults::bool_true")]
1418    pub enabled: bool,
1419    /// Volume 0-100 (0 effectively disables)
1420    #[serde(default = "crate::defaults::bell_sound")]
1421    pub volume: u8,
1422    /// Optional path to a custom sound file (WAV/OGG/FLAC).
1423    /// If None, uses built-in tone with the configured frequency.
1424    #[serde(default)]
1425    pub sound_file: Option<String>,
1426    /// Frequency in Hz for the built-in tone (used when sound_file is None)
1427    #[serde(default = "default_alert_frequency")]
1428    pub frequency: f32,
1429    /// Duration of the built-in tone in milliseconds
1430    #[serde(default = "default_alert_duration_ms")]
1431    pub duration_ms: u64,
1432}
1433
1434fn default_alert_frequency() -> f32 {
1435    800.0
1436}
1437
1438fn default_alert_duration_ms() -> u64 {
1439    100
1440}
1441
1442impl Default for AlertSoundConfig {
1443    fn default() -> Self {
1444        Self {
1445            enabled: true,
1446            volume: 50,
1447            sound_file: None,
1448            frequency: 800.0,
1449            duration_ms: 100,
1450        }
1451    }
1452}
1453
1454/// A smart selection rule for pattern-based text selection.
1455///
1456/// When double-clicking, rules are evaluated by precision (highest first).
1457/// If a pattern matches at the cursor position, that text is selected.
1458#[derive(Debug, Clone, Serialize, Deserialize)]
1459pub struct SmartSelectionRule {
1460    /// Human-readable name for this rule (e.g., "HTTP URL", "Email address")
1461    pub name: String,
1462    /// Regular expression pattern to match
1463    pub regex: String,
1464    /// Precision level - higher precision rules are checked first
1465    #[serde(default)]
1466    pub precision: SmartSelectionPrecision,
1467    /// Whether this rule is enabled
1468    #[serde(default = "default_enabled")]
1469    pub enabled: bool,
1470}
1471
1472fn default_enabled() -> bool {
1473    true
1474}
1475
1476impl SmartSelectionRule {
1477    /// Create a new smart selection rule
1478    pub fn new(
1479        name: impl Into<String>,
1480        regex: impl Into<String>,
1481        precision: SmartSelectionPrecision,
1482    ) -> Self {
1483        Self {
1484            name: name.into(),
1485            regex: regex.into(),
1486            precision,
1487            enabled: true,
1488        }
1489    }
1490}
1491
1492/// Get the default smart selection rules (based on iTerm2's defaults)
1493pub fn default_smart_selection_rules() -> Vec<SmartSelectionRule> {
1494    vec![
1495        // Very High precision - most specific, checked first
1496        SmartSelectionRule::new(
1497            "HTTP URL",
1498            r"https?://[^\s<>\[\]{}|\\^`\x00-\x1f]+",
1499            SmartSelectionPrecision::VeryHigh,
1500        ),
1501        SmartSelectionRule::new(
1502            "SSH URL",
1503            r"\bssh://([a-zA-Z0-9_]+@)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+(/[^\s]*)?",
1504            SmartSelectionPrecision::VeryHigh,
1505        ),
1506        SmartSelectionRule::new(
1507            "Git URL",
1508            r"\bgit://([a-zA-Z0-9_]+@)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+(/[^\s]*)?",
1509            SmartSelectionPrecision::VeryHigh,
1510        ),
1511        SmartSelectionRule::new(
1512            "File URL",
1513            r"file://[^\s]+",
1514            SmartSelectionPrecision::VeryHigh,
1515        ),
1516        // High precision
1517        SmartSelectionRule::new(
1518            "Email address",
1519            r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
1520            SmartSelectionPrecision::High,
1521        ),
1522        SmartSelectionRule::new(
1523            "IPv4 address",
1524            r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b",
1525            SmartSelectionPrecision::High,
1526        ),
1527        // Normal precision
1528        SmartSelectionRule::new(
1529            "File path",
1530            r"~?/?(?:[a-zA-Z0-9._-]+/)+[a-zA-Z0-9._-]+/?",
1531            SmartSelectionPrecision::Normal,
1532        ),
1533        SmartSelectionRule::new(
1534            "Java/Python import",
1535            // Require at least 2 dots to avoid matching simple filenames like "file.txt"
1536            r"(?:[a-zA-Z_][a-zA-Z0-9_]*\.){2,}[a-zA-Z_][a-zA-Z0-9_]*",
1537            SmartSelectionPrecision::Normal,
1538        ),
1539        SmartSelectionRule::new(
1540            "C++ namespace",
1541            r"(?:[a-zA-Z_][a-zA-Z0-9_]*::)+[a-zA-Z_][a-zA-Z0-9_]*",
1542            SmartSelectionPrecision::Normal,
1543        ),
1544        SmartSelectionRule::new(
1545            "Quoted string",
1546            r#""(?:[^"\\]|\\.)*""#,
1547            SmartSelectionPrecision::Normal,
1548        ),
1549        SmartSelectionRule::new(
1550            "UUID",
1551            r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b",
1552            SmartSelectionPrecision::Normal,
1553        ),
1554        // Note: No "whitespace-bounded" catch-all pattern here - that would defeat
1555        // the purpose of configurable word_characters. If no smart pattern matches,
1556        // selection falls back to word boundary detection using word_characters.
1557    ]
1558}
1559
1560// ============================================================================
1561// Rendering Types
1562// ============================================================================
1563
1564/// Per-pane background image configuration (runtime state)
1565#[derive(Debug, Clone, Default)]
1566pub struct PaneBackground {
1567    /// Path to the background image (None = use global background)
1568    pub image_path: Option<String>,
1569    /// Display mode (fit/fill/stretch/tile/center)
1570    pub mode: BackgroundImageMode,
1571    /// Opacity (0.0-1.0)
1572    pub opacity: f32,
1573}
1574
1575impl PaneBackground {
1576    /// Create a new PaneBackground with default settings
1577    pub fn new() -> Self {
1578        Self {
1579            image_path: None,
1580            mode: BackgroundImageMode::default(),
1581            opacity: 1.0,
1582        }
1583    }
1584
1585    /// Returns true if this pane has a custom background image set
1586    pub fn has_image(&self) -> bool {
1587        self.image_path.is_some()
1588    }
1589}
1590
1591/// A divider rectangle between panes
1592#[derive(Debug, Clone, Copy)]
1593pub struct DividerRect {
1594    /// X position in pixels
1595    pub x: f32,
1596    /// Y position in pixels
1597    pub y: f32,
1598    /// Width in pixels
1599    pub width: f32,
1600    /// Height in pixels
1601    pub height: f32,
1602    /// Whether this is a horizontal divider (vertical line)
1603    pub is_horizontal: bool,
1604}
1605
1606impl DividerRect {
1607    /// Create a new divider rect
1608    pub fn new(x: f32, y: f32, width: f32, height: f32, is_horizontal: bool) -> Self {
1609        Self {
1610            x,
1611            y,
1612            width,
1613            height,
1614            is_horizontal,
1615        }
1616    }
1617
1618    /// Check if a point is inside the divider (with optional padding for easier grabbing)
1619    pub fn contains(&self, px: f32, py: f32, padding: f32) -> bool {
1620        px >= self.x - padding
1621            && px < self.x + self.width + padding
1622            && py >= self.y - padding
1623            && py < self.y + self.height + padding
1624    }
1625}
1626
1627/// Visible command separator mark: (row, col_offset, optional_color)
1628pub type SeparatorMark = (usize, Option<i32>, Option<(u8, u8, u8)>);
1629
1630// ============================================================================
1631// Shared ID Types
1632// ============================================================================
1633
1634/// Unique identifier for a pane
1635pub type PaneId = u64;
1636
1637/// Unique identifier for a tab
1638pub type TabId = u64;