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/// Style for link highlight underlines.
1039#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1040#[serde(rename_all = "snake_case")]
1041pub enum LinkUnderlineStyle {
1042    /// Solid continuous underline
1043    Solid,
1044    /// Dotted/stipple underline (alternating pixels)
1045    #[default]
1046    Stipple,
1047}
1048
1049impl LinkUnderlineStyle {
1050    /// Display name for UI
1051    pub fn display_name(&self) -> &'static str {
1052        match self {
1053            LinkUnderlineStyle::Solid => "Solid",
1054            LinkUnderlineStyle::Stipple => "Stipple",
1055        }
1056    }
1057
1058    /// All available styles for UI iteration
1059    pub fn all() -> &'static [LinkUnderlineStyle] {
1060        &[LinkUnderlineStyle::Solid, LinkUnderlineStyle::Stipple]
1061    }
1062}
1063
1064// ============================================================================
1065// Per-Shader Configuration Types
1066// ============================================================================
1067
1068/// Metadata embedded in shader files via YAML block comments.
1069///
1070/// Parsed from `/*! par-term shader metadata ... */` blocks at the top of shader files.
1071#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1072pub struct ShaderMetadata {
1073    /// Human-readable name for the shader (e.g., "CRT Effect")
1074    pub name: Option<String>,
1075    /// Author of the shader
1076    pub author: Option<String>,
1077    /// Description of what the shader does
1078    pub description: Option<String>,
1079    /// Version string (e.g., "1.0.0")
1080    pub version: Option<String>,
1081    /// Default configuration values for this shader
1082    #[serde(default)]
1083    pub defaults: ShaderConfig,
1084}
1085
1086/// Per-shader configuration settings.
1087///
1088/// Used both for embedded defaults in shader files and for user overrides in config.yaml.
1089/// All fields are optional to allow partial overrides.
1090#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1091pub struct ShaderConfig {
1092    /// Animation speed multiplier (1.0 = normal speed)
1093    pub animation_speed: Option<f32>,
1094    /// Brightness multiplier (0.05-1.0)
1095    pub brightness: Option<f32>,
1096    /// Text opacity when using this shader (0.0-1.0)
1097    pub text_opacity: Option<f32>,
1098    /// When true, shader receives full terminal content for manipulation
1099    pub full_content: Option<bool>,
1100    /// Path to texture for iChannel0
1101    pub channel0: Option<String>,
1102    /// Path to texture for iChannel1
1103    pub channel1: Option<String>,
1104    /// Path to texture for iChannel2
1105    pub channel2: Option<String>,
1106    /// Path to texture for iChannel3
1107    pub channel3: Option<String>,
1108    /// Path prefix for cubemap faces
1109    pub cubemap: Option<String>,
1110    /// Whether cubemap sampling is enabled
1111    pub cubemap_enabled: Option<bool>,
1112    /// Use the app's background image as iChannel0 instead of a separate texture
1113    pub use_background_as_channel0: Option<bool>,
1114}
1115
1116/// Cursor shader specific configuration.
1117///
1118/// Extends base ShaderConfig with cursor-specific settings.
1119#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1120pub struct CursorShaderConfig {
1121    /// Base shader configuration
1122    #[serde(flatten)]
1123    pub base: ShaderConfig,
1124    /// Hide the default cursor when this shader is enabled
1125    pub hides_cursor: Option<bool>,
1126    /// Disable cursor shader while in alt screen (vim, less, htop)
1127    pub disable_in_alt_screen: Option<bool>,
1128    /// Cursor glow radius in pixels
1129    pub glow_radius: Option<f32>,
1130    /// Cursor glow intensity (0.0-1.0)
1131    pub glow_intensity: Option<f32>,
1132    /// Duration of cursor trail effect in seconds
1133    pub trail_duration: Option<f32>,
1134    /// Cursor color for shader effects [R, G, B] (0-255)
1135    pub cursor_color: Option<[u8; 3]>,
1136}
1137
1138/// Metadata embedded in cursor shader files via YAML block comments.
1139///
1140/// Parsed from `/*! par-term shader metadata ... */` blocks at the top of cursor shader files.
1141/// Similar to `ShaderMetadata` but with cursor-specific defaults.
1142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1143pub struct CursorShaderMetadata {
1144    /// Human-readable name for the shader (e.g., "Cursor Glow Effect")
1145    pub name: Option<String>,
1146    /// Author of the shader
1147    pub author: Option<String>,
1148    /// Description of what the shader does
1149    pub description: Option<String>,
1150    /// Version string (e.g., "1.0.0")
1151    pub version: Option<String>,
1152    /// Default configuration values for this cursor shader
1153    #[serde(default)]
1154    pub defaults: CursorShaderConfig,
1155}
1156
1157/// Fully resolved shader configuration with all values filled in.
1158///
1159/// Created by merging user overrides, shader metadata defaults, and global defaults.
1160#[derive(Debug, Clone)]
1161#[allow(dead_code)]
1162pub struct ResolvedShaderConfig {
1163    /// Animation speed multiplier
1164    pub animation_speed: f32,
1165    /// Brightness multiplier
1166    pub brightness: f32,
1167    /// Text opacity
1168    pub text_opacity: f32,
1169    /// Full content mode enabled
1170    pub full_content: bool,
1171    /// Resolved path to iChannel0 texture
1172    pub channel0: Option<PathBuf>,
1173    /// Resolved path to iChannel1 texture
1174    pub channel1: Option<PathBuf>,
1175    /// Resolved path to iChannel2 texture
1176    pub channel2: Option<PathBuf>,
1177    /// Resolved path to iChannel3 texture
1178    pub channel3: Option<PathBuf>,
1179    /// Resolved cubemap path prefix
1180    pub cubemap: Option<PathBuf>,
1181    /// Cubemap sampling enabled
1182    pub cubemap_enabled: bool,
1183    /// Use the app's background image as iChannel0
1184    pub use_background_as_channel0: bool,
1185}
1186
1187impl Default for ResolvedShaderConfig {
1188    fn default() -> Self {
1189        Self {
1190            animation_speed: 1.0,
1191            brightness: 1.0,
1192            text_opacity: 1.0,
1193            full_content: false,
1194            channel0: None,
1195            channel1: None,
1196            channel2: None,
1197            channel3: None,
1198            cubemap: None,
1199            cubemap_enabled: true,
1200            use_background_as_channel0: false,
1201        }
1202    }
1203}
1204
1205/// Fully resolved cursor shader configuration with all values filled in.
1206#[derive(Debug, Clone)]
1207#[allow(dead_code)]
1208pub struct ResolvedCursorShaderConfig {
1209    /// Base resolved shader config
1210    pub base: ResolvedShaderConfig,
1211    /// Hide the default cursor when this shader is enabled
1212    pub hides_cursor: bool,
1213    /// Disable cursor shader while in alt screen (vim, less, htop)
1214    pub disable_in_alt_screen: bool,
1215    /// Cursor glow radius in pixels
1216    pub glow_radius: f32,
1217    /// Cursor glow intensity (0.0-1.0)
1218    pub glow_intensity: f32,
1219    /// Duration of cursor trail effect in seconds
1220    pub trail_duration: f32,
1221    /// Cursor color for shader effects [R, G, B] (0-255)
1222    pub cursor_color: [u8; 3],
1223}
1224
1225impl Default for ResolvedCursorShaderConfig {
1226    fn default() -> Self {
1227        Self {
1228            base: ResolvedShaderConfig::default(),
1229            hides_cursor: false,
1230            disable_in_alt_screen: true,
1231            glow_radius: 80.0,
1232            glow_intensity: 0.3,
1233            trail_duration: 0.5,
1234            cursor_color: [255, 255, 255],
1235        }
1236    }
1237}
1238
1239// ============================================================================
1240// Progress Bar Types
1241// ============================================================================
1242
1243/// Progress bar visual style
1244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1245#[serde(rename_all = "lowercase")]
1246pub enum ProgressBarStyle {
1247    /// Thin bar line (default)
1248    #[default]
1249    Bar,
1250    /// Bar with percentage text
1251    BarWithText,
1252}
1253
1254impl ProgressBarStyle {
1255    /// Display name for UI
1256    pub fn display_name(&self) -> &'static str {
1257        match self {
1258            ProgressBarStyle::Bar => "Bar",
1259            ProgressBarStyle::BarWithText => "Bar with Text",
1260        }
1261    }
1262
1263    /// All available styles for UI iteration
1264    pub fn all() -> &'static [ProgressBarStyle] {
1265        &[ProgressBarStyle::Bar, ProgressBarStyle::BarWithText]
1266    }
1267}
1268
1269/// Progress bar position on screen
1270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1271#[serde(rename_all = "lowercase")]
1272pub enum ProgressBarPosition {
1273    /// Top of the terminal window (default)
1274    #[default]
1275    Top,
1276    /// Bottom of the terminal window
1277    Bottom,
1278}
1279
1280impl ProgressBarPosition {
1281    /// Display name for UI
1282    pub fn display_name(&self) -> &'static str {
1283        match self {
1284            ProgressBarPosition::Bottom => "Bottom",
1285            ProgressBarPosition::Top => "Top",
1286        }
1287    }
1288
1289    /// All available positions for UI iteration
1290    pub fn all() -> &'static [ProgressBarPosition] {
1291        &[ProgressBarPosition::Top, ProgressBarPosition::Bottom]
1292    }
1293}
1294
1295// ============================================================================
1296// Smart Selection Types
1297// ============================================================================
1298
1299/// Precision level for smart selection rules.
1300///
1301/// Higher precision rules are checked first and match more specific patterns.
1302/// Based on iTerm2's smart selection precision levels.
1303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1304#[serde(rename_all = "snake_case")]
1305pub enum SmartSelectionPrecision {
1306    /// Very low precision (0.00001) - matches almost anything
1307    VeryLow,
1308    /// Low precision (0.001) - broad patterns
1309    Low,
1310    /// Normal precision (1.0) - standard patterns
1311    #[default]
1312    Normal,
1313    /// High precision (1000.0) - specific patterns
1314    High,
1315    /// Very high precision (1000000.0) - most specific patterns (checked first)
1316    VeryHigh,
1317}
1318
1319impl SmartSelectionPrecision {
1320    /// Get the numeric precision value for sorting
1321    pub fn value(&self) -> f64 {
1322        match self {
1323            SmartSelectionPrecision::VeryLow => 0.00001,
1324            SmartSelectionPrecision::Low => 0.001,
1325            SmartSelectionPrecision::Normal => 1.0,
1326            SmartSelectionPrecision::High => 1000.0,
1327            SmartSelectionPrecision::VeryHigh => 1_000_000.0,
1328        }
1329    }
1330
1331    /// Display name for UI
1332    pub fn display_name(&self) -> &'static str {
1333        match self {
1334            SmartSelectionPrecision::VeryLow => "Very Low",
1335            SmartSelectionPrecision::Low => "Low",
1336            SmartSelectionPrecision::Normal => "Normal",
1337            SmartSelectionPrecision::High => "High",
1338            SmartSelectionPrecision::VeryHigh => "Very High",
1339        }
1340    }
1341}
1342
1343/// Position of pane title bars
1344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1345#[serde(rename_all = "snake_case")]
1346pub enum PaneTitlePosition {
1347    /// Title bar at the top of the pane (default)
1348    #[default]
1349    Top,
1350    /// Title bar at the bottom of the pane
1351    Bottom,
1352}
1353
1354impl PaneTitlePosition {
1355    /// All available positions for UI dropdowns
1356    pub const ALL: &'static [PaneTitlePosition] =
1357        &[PaneTitlePosition::Top, PaneTitlePosition::Bottom];
1358
1359    /// Display name for UI
1360    pub fn display_name(&self) -> &'static str {
1361        match self {
1362            PaneTitlePosition::Top => "Top",
1363            PaneTitlePosition::Bottom => "Bottom",
1364        }
1365    }
1366}
1367
1368/// Style of dividers between panes
1369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1370#[serde(rename_all = "snake_case")]
1371pub enum DividerStyle {
1372    /// Solid line (default)
1373    #[default]
1374    Solid,
1375    /// Double line effect (two thin lines with gap)
1376    Double,
1377    /// Dashed line effect
1378    Dashed,
1379    /// Shadow effect (gradient fade)
1380    Shadow,
1381}
1382
1383impl DividerStyle {
1384    /// All available styles for UI dropdowns
1385    pub const ALL: &'static [DividerStyle] = &[
1386        DividerStyle::Solid,
1387        DividerStyle::Double,
1388        DividerStyle::Dashed,
1389        DividerStyle::Shadow,
1390    ];
1391
1392    /// Display name for UI
1393    pub fn display_name(&self) -> &'static str {
1394        match self {
1395            DividerStyle::Solid => "Solid",
1396            DividerStyle::Double => "Double",
1397            DividerStyle::Dashed => "Dashed",
1398            DividerStyle::Shadow => "Shadow",
1399        }
1400    }
1401}
1402
1403/// Terminal events that can trigger alert sounds
1404#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1405#[serde(rename_all = "snake_case")]
1406pub enum AlertEvent {
1407    /// Bell character received (BEL / 0x07)
1408    Bell,
1409    /// Command completed (requires shell integration)
1410    CommandComplete,
1411    /// A new tab was created
1412    NewTab,
1413    /// A tab was closed
1414    TabClose,
1415}
1416
1417impl AlertEvent {
1418    /// Display name for UI
1419    pub fn display_name(&self) -> &'static str {
1420        match self {
1421            AlertEvent::Bell => "Bell",
1422            AlertEvent::CommandComplete => "Command Complete",
1423            AlertEvent::NewTab => "New Tab",
1424            AlertEvent::TabClose => "Tab Close",
1425        }
1426    }
1427
1428    /// All available events for UI iteration
1429    pub fn all() -> &'static [AlertEvent] {
1430        &[
1431            AlertEvent::Bell,
1432            AlertEvent::CommandComplete,
1433            AlertEvent::NewTab,
1434            AlertEvent::TabClose,
1435        ]
1436    }
1437}
1438
1439/// Configuration for an alert sound tied to a specific event
1440#[derive(Debug, Clone, Serialize, Deserialize)]
1441pub struct AlertSoundConfig {
1442    /// Whether this alert sound is enabled
1443    #[serde(default = "crate::defaults::bool_true")]
1444    pub enabled: bool,
1445    /// Volume 0-100 (0 effectively disables)
1446    #[serde(default = "crate::defaults::bell_sound")]
1447    pub volume: u8,
1448    /// Optional path to a custom sound file (WAV/OGG/FLAC).
1449    /// If None, uses built-in tone with the configured frequency.
1450    #[serde(default)]
1451    pub sound_file: Option<String>,
1452    /// Frequency in Hz for the built-in tone (used when sound_file is None)
1453    #[serde(default = "default_alert_frequency")]
1454    pub frequency: f32,
1455    /// Duration of the built-in tone in milliseconds
1456    #[serde(default = "default_alert_duration_ms")]
1457    pub duration_ms: u64,
1458}
1459
1460fn default_alert_frequency() -> f32 {
1461    800.0
1462}
1463
1464fn default_alert_duration_ms() -> u64 {
1465    100
1466}
1467
1468impl Default for AlertSoundConfig {
1469    fn default() -> Self {
1470        Self {
1471            enabled: true,
1472            volume: 50,
1473            sound_file: None,
1474            frequency: 800.0,
1475            duration_ms: 100,
1476        }
1477    }
1478}
1479
1480/// A smart selection rule for pattern-based text selection.
1481///
1482/// When double-clicking, rules are evaluated by precision (highest first).
1483/// If a pattern matches at the cursor position, that text is selected.
1484#[derive(Debug, Clone, Serialize, Deserialize)]
1485pub struct SmartSelectionRule {
1486    /// Human-readable name for this rule (e.g., "HTTP URL", "Email address")
1487    pub name: String,
1488    /// Regular expression pattern to match
1489    pub regex: String,
1490    /// Precision level - higher precision rules are checked first
1491    #[serde(default)]
1492    pub precision: SmartSelectionPrecision,
1493    /// Whether this rule is enabled
1494    #[serde(default = "default_enabled")]
1495    pub enabled: bool,
1496}
1497
1498fn default_enabled() -> bool {
1499    true
1500}
1501
1502impl SmartSelectionRule {
1503    /// Create a new smart selection rule
1504    pub fn new(
1505        name: impl Into<String>,
1506        regex: impl Into<String>,
1507        precision: SmartSelectionPrecision,
1508    ) -> Self {
1509        Self {
1510            name: name.into(),
1511            regex: regex.into(),
1512            precision,
1513            enabled: true,
1514        }
1515    }
1516}
1517
1518/// Get the default smart selection rules (based on iTerm2's defaults)
1519pub fn default_smart_selection_rules() -> Vec<SmartSelectionRule> {
1520    vec![
1521        // Very High precision - most specific, checked first
1522        SmartSelectionRule::new(
1523            "HTTP URL",
1524            r"https?://[^\s<>\[\]{}|\\^`\x00-\x1f]+",
1525            SmartSelectionPrecision::VeryHigh,
1526        ),
1527        SmartSelectionRule::new(
1528            "SSH URL",
1529            r"\bssh://([a-zA-Z0-9_]+@)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+(/[^\s]*)?",
1530            SmartSelectionPrecision::VeryHigh,
1531        ),
1532        SmartSelectionRule::new(
1533            "Git URL",
1534            r"\bgit://([a-zA-Z0-9_]+@)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+(/[^\s]*)?",
1535            SmartSelectionPrecision::VeryHigh,
1536        ),
1537        SmartSelectionRule::new(
1538            "File URL",
1539            r"file://[^\s]+",
1540            SmartSelectionPrecision::VeryHigh,
1541        ),
1542        // High precision
1543        SmartSelectionRule::new(
1544            "Email address",
1545            r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
1546            SmartSelectionPrecision::High,
1547        ),
1548        SmartSelectionRule::new(
1549            "IPv4 address",
1550            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",
1551            SmartSelectionPrecision::High,
1552        ),
1553        // Normal precision
1554        SmartSelectionRule::new(
1555            "File path",
1556            r"~?/?(?:[a-zA-Z0-9._-]+/)+[a-zA-Z0-9._-]+/?",
1557            SmartSelectionPrecision::Normal,
1558        ),
1559        SmartSelectionRule::new(
1560            "Java/Python import",
1561            // Require at least 2 dots to avoid matching simple filenames like "file.txt"
1562            r"(?:[a-zA-Z_][a-zA-Z0-9_]*\.){2,}[a-zA-Z_][a-zA-Z0-9_]*",
1563            SmartSelectionPrecision::Normal,
1564        ),
1565        SmartSelectionRule::new(
1566            "C++ namespace",
1567            r"(?:[a-zA-Z_][a-zA-Z0-9_]*::)+[a-zA-Z_][a-zA-Z0-9_]*",
1568            SmartSelectionPrecision::Normal,
1569        ),
1570        SmartSelectionRule::new(
1571            "Quoted string",
1572            r#""(?:[^"\\]|\\.)*""#,
1573            SmartSelectionPrecision::Normal,
1574        ),
1575        SmartSelectionRule::new(
1576            "UUID",
1577            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",
1578            SmartSelectionPrecision::Normal,
1579        ),
1580        // Note: No "whitespace-bounded" catch-all pattern here - that would defeat
1581        // the purpose of configurable word_characters. If no smart pattern matches,
1582        // selection falls back to word boundary detection using word_characters.
1583    ]
1584}
1585
1586// ============================================================================
1587// Rendering Types
1588// ============================================================================
1589
1590/// Per-pane background image configuration (runtime state)
1591#[derive(Debug, Clone, Default)]
1592pub struct PaneBackground {
1593    /// Path to the background image (None = use global background)
1594    pub image_path: Option<String>,
1595    /// Display mode (fit/fill/stretch/tile/center)
1596    pub mode: BackgroundImageMode,
1597    /// Opacity (0.0-1.0)
1598    pub opacity: f32,
1599}
1600
1601impl PaneBackground {
1602    /// Create a new PaneBackground with default settings
1603    pub fn new() -> Self {
1604        Self {
1605            image_path: None,
1606            mode: BackgroundImageMode::default(),
1607            opacity: 1.0,
1608        }
1609    }
1610
1611    /// Returns true if this pane has a custom background image set
1612    pub fn has_image(&self) -> bool {
1613        self.image_path.is_some()
1614    }
1615}
1616
1617/// A divider rectangle between panes
1618#[derive(Debug, Clone, Copy)]
1619pub struct DividerRect {
1620    /// X position in pixels
1621    pub x: f32,
1622    /// Y position in pixels
1623    pub y: f32,
1624    /// Width in pixels
1625    pub width: f32,
1626    /// Height in pixels
1627    pub height: f32,
1628    /// Whether this is a horizontal divider (vertical line)
1629    pub is_horizontal: bool,
1630}
1631
1632impl DividerRect {
1633    /// Create a new divider rect
1634    pub fn new(x: f32, y: f32, width: f32, height: f32, is_horizontal: bool) -> Self {
1635        Self {
1636            x,
1637            y,
1638            width,
1639            height,
1640            is_horizontal,
1641        }
1642    }
1643
1644    /// Check if a point is inside the divider (with optional padding for easier grabbing)
1645    pub fn contains(&self, px: f32, py: f32, padding: f32) -> bool {
1646        px >= self.x - padding
1647            && px < self.x + self.width + padding
1648            && py >= self.y - padding
1649            && py < self.y + self.height + padding
1650    }
1651}
1652
1653/// Visible command separator mark: (row, col_offset, optional_color)
1654pub type SeparatorMark = (usize, Option<i32>, Option<(u8, u8, u8)>);
1655
1656// ============================================================================
1657// Shared ID Types
1658// ============================================================================
1659
1660/// Unique identifier for a pane
1661pub type PaneId = u64;
1662
1663/// Unique identifier for a tab
1664pub type TabId = u64;