mermaid_cli/tui/
mode.rs

1use ratatui::style::Color;
2use serde::{Deserialize, Serialize};
3
4/// The four operation modes for Mermaid, inspired by Claude Code's Shift+Tab cycling
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum OperationMode {
7    /// Default mode - asks for confirmation on all operations
8    Normal,
9    /// Auto-accepts file edits only, confirms other operations
10    AcceptEdits,
11    /// Research & plan mode - shows what would happen without executing
12    PlanMode,
13    /// YOLO mode - accepts everything automatically (use with caution)
14    BypassAll,
15}
16
17impl Default for OperationMode {
18    fn default() -> Self {
19        Self::Normal
20    }
21}
22
23impl OperationMode {
24    /// Cycle to the next mode in the sequence
25    pub fn cycle(&self) -> Self {
26        match self {
27            Self::Normal => Self::AcceptEdits,
28            Self::AcceptEdits => Self::PlanMode,
29            Self::PlanMode => Self::BypassAll,
30            Self::BypassAll => Self::Normal,
31        }
32    }
33
34    /// Cycle to the previous mode in the sequence
35    pub fn cycle_reverse(&self) -> Self {
36        match self {
37            Self::Normal => Self::BypassAll,
38            Self::BypassAll => Self::PlanMode,
39            Self::PlanMode => Self::AcceptEdits,
40            Self::AcceptEdits => Self::Normal,
41        }
42    }
43
44    /// Get the display name for the mode with icon
45    pub fn display_name(&self) -> &str {
46        match self {
47            Self::Normal => "Normal",
48            Self::AcceptEdits => "Accept Edits",
49            Self::PlanMode => "Plan Mode",
50            Self::BypassAll => "Bypass All",
51        }
52    }
53
54    /// Get a short name for compact display
55    pub fn short_name(&self) -> &str {
56        match self {
57            Self::Normal => "N",
58            Self::AcceptEdits => "A",
59            Self::PlanMode => "P",
60            Self::BypassAll => "B",
61        }
62    }
63
64    /// Get the color associated with this mode for visual indicators
65    pub fn color(&self) -> Color {
66        match self {
67            Self::Normal => Color::Green,
68            Self::AcceptEdits => Color::Yellow,
69            Self::PlanMode => Color::Blue,
70            Self::BypassAll => Color::Red,
71        }
72    }
73
74    /// Get a description of what this mode does
75    pub fn description(&self) -> &str {
76        match self {
77            Self::Normal => "Asks for confirmation on all operations",
78            Self::AcceptEdits => "Auto-accepts file edits, confirms other operations",
79            Self::PlanMode => "Shows what would happen without executing anything",
80            Self::BypassAll => "Automatically accepts all operations (use with caution)",
81        }
82    }
83
84    /// Check if this mode should auto-accept file operations
85    pub fn auto_accept_files(&self) -> bool {
86        matches!(self, Self::AcceptEdits | Self::BypassAll)
87    }
88
89    /// Check if this mode should auto-accept shell commands
90    pub fn auto_accept_commands(&self) -> bool {
91        matches!(self, Self::BypassAll)
92    }
93
94    /// Check if this mode should auto-accept git operations
95    pub fn auto_accept_git(&self) -> bool {
96        matches!(self, Self::BypassAll)
97    }
98
99    /// Check if this mode is in planning-only mode (no execution)
100    pub fn is_planning_only(&self) -> bool {
101        matches!(self, Self::PlanMode)
102    }
103
104    /// Check if this mode requires extra safety checks
105    pub fn needs_safety_confirmation(&self) -> bool {
106        matches!(self, Self::BypassAll)
107    }
108
109    /// Get the keyboard hint for this mode
110    pub fn keyboard_hint(&self) -> &str {
111        match self {
112            Self::Normal => "Shift+Tab to cycle modes",
113            Self::AcceptEdits => "Ctrl+E for Accept Edits mode",
114            Self::PlanMode => "Ctrl+P for Plan mode",
115            Self::BypassAll => "Ctrl+Y to toggle Bypass All",
116        }
117    }
118
119    /// Parse mode from string (for config files)
120    pub fn from_str(s: &str) -> Option<Self> {
121        match s.to_lowercase().as_str() {
122            "normal" => Some(Self::Normal),
123            "accept_edits" | "accept-edits" | "acceptedits" => Some(Self::AcceptEdits),
124            "plan_mode" | "plan-mode" | "planmode" | "plan" => Some(Self::PlanMode),
125            "bypass_all" | "bypass-all" | "bypassall" | "bypass" | "yolo" => Some(Self::BypassAll),
126            _ => None,
127        }
128    }
129
130    /// Convert mode to string (for config files)
131    pub fn to_str(&self) -> &str {
132        match self {
133            Self::Normal => "normal",
134            Self::AcceptEdits => "accept_edits",
135            Self::PlanMode => "plan_mode",
136            Self::BypassAll => "bypass_all",
137        }
138    }
139
140    /// Get the warning level for this mode
141    pub fn warning_level(&self) -> WarningLevel {
142        match self {
143            Self::Normal => WarningLevel::None,
144            Self::AcceptEdits => WarningLevel::Low,
145            Self::PlanMode => WarningLevel::None,
146            Self::BypassAll => WarningLevel::High,
147        }
148    }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum WarningLevel {
153    None,
154    Low,
155    High,
156}
157
158impl WarningLevel {
159    pub fn color(&self) -> Color {
160        match self {
161            Self::None => Color::Green,
162            Self::Low => Color::Yellow,
163            Self::High => Color::Red,
164        }
165    }
166
167    pub fn message(&self) -> Option<&str> {
168        match self {
169            Self::None => None,
170            Self::Low => Some("Auto-accept files"),
171            Self::High => Some("WARNING: BYPASS ALL"),
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_mode_cycling() {
182        let mut mode = OperationMode::Normal;
183
184        mode = mode.cycle();
185        assert_eq!(mode, OperationMode::AcceptEdits);
186
187        mode = mode.cycle();
188        assert_eq!(mode, OperationMode::PlanMode);
189
190        mode = mode.cycle();
191        assert_eq!(mode, OperationMode::BypassAll);
192
193        mode = mode.cycle();
194        assert_eq!(mode, OperationMode::Normal);
195    }
196
197    #[test]
198    fn test_mode_cycling_reverse() {
199        let mut mode = OperationMode::Normal;
200
201        mode = mode.cycle_reverse();
202        assert_eq!(mode, OperationMode::BypassAll);
203
204        mode = mode.cycle_reverse();
205        assert_eq!(mode, OperationMode::PlanMode);
206
207        mode = mode.cycle_reverse();
208        assert_eq!(mode, OperationMode::AcceptEdits);
209
210        mode = mode.cycle_reverse();
211        assert_eq!(mode, OperationMode::Normal);
212    }
213
214    #[test]
215    fn test_mode_permissions() {
216        assert!(!OperationMode::Normal.auto_accept_files());
217        assert!(!OperationMode::Normal.auto_accept_commands());
218
219        assert!(OperationMode::AcceptEdits.auto_accept_files());
220        assert!(!OperationMode::AcceptEdits.auto_accept_commands());
221
222        assert!(!OperationMode::PlanMode.auto_accept_files());
223        assert!(OperationMode::PlanMode.is_planning_only());
224
225        assert!(OperationMode::BypassAll.auto_accept_files());
226        assert!(OperationMode::BypassAll.auto_accept_commands());
227        assert!(OperationMode::BypassAll.auto_accept_git());
228    }
229
230    #[test]
231    fn test_mode_from_str() {
232        assert_eq!(
233            OperationMode::from_str("normal"),
234            Some(OperationMode::Normal)
235        );
236        assert_eq!(
237            OperationMode::from_str("accept_edits"),
238            Some(OperationMode::AcceptEdits)
239        );
240        assert_eq!(
241            OperationMode::from_str("plan-mode"),
242            Some(OperationMode::PlanMode)
243        );
244        assert_eq!(
245            OperationMode::from_str("yolo"),
246            Some(OperationMode::BypassAll)
247        );
248        assert_eq!(OperationMode::from_str("invalid"), None);
249    }
250}