Skip to main content

ratatui_interact/utils/
mouse_capture.rs

1//! Mouse capture state management
2//!
3//! Provides utilities for toggling mouse capture at runtime, enabling "copy mode"
4//! where the terminal's native text selection is available instead of ratatui
5//! capturing mouse events.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use ratatui_interact::utils::{MouseCaptureState, toggle_mouse_capture};
11//! use std::io;
12//!
13//! let mut stdout = io::stdout();
14//! let mut capture_state = MouseCaptureState::new(true); // Start with capture enabled
15//!
16//! // Toggle mouse capture (e.g., when user presses 'm')
17//! toggle_mouse_capture(&mut stdout, &mut capture_state)?;
18//!
19//! // Check if we're in copy mode (capture disabled)
20//! if capture_state.is_copy_mode() {
21//!     println!("Select text with your mouse!");
22//! }
23//! ```
24
25use std::io::{self, Write};
26
27use crossterm::{
28    event::{DisableMouseCapture, EnableMouseCapture},
29    execute,
30};
31
32/// State for mouse capture management
33///
34/// Tracks whether mouse capture is enabled and provides methods to toggle it.
35/// When mouse capture is disabled, the terminal allows native text selection
36/// (copy mode).
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct MouseCaptureState {
39    /// Whether mouse capture is currently enabled
40    enabled: bool,
41}
42
43impl MouseCaptureState {
44    /// Create a new mouse capture state
45    ///
46    /// # Arguments
47    /// * `enabled` - Initial state (true = capturing mouse events)
48    pub fn new(enabled: bool) -> Self {
49        Self { enabled }
50    }
51
52    /// Create a new state with mouse capture enabled
53    pub fn enabled() -> Self {
54        Self::new(true)
55    }
56
57    /// Create a new state with mouse capture disabled (copy mode)
58    pub fn disabled() -> Self {
59        Self::new(false)
60    }
61
62    /// Check if mouse capture is currently enabled
63    pub fn is_enabled(&self) -> bool {
64        self.enabled
65    }
66
67    /// Check if copy mode is active (mouse capture disabled)
68    ///
69    /// When in copy mode, the terminal allows native text selection.
70    pub fn is_copy_mode(&self) -> bool {
71        !self.enabled
72    }
73
74    /// Set the mouse capture state
75    pub fn set_enabled(&mut self, enabled: bool) {
76        self.enabled = enabled;
77    }
78
79    /// Toggle the mouse capture state and return the new state
80    pub fn toggle(&mut self) -> bool {
81        self.enabled = !self.enabled;
82        self.enabled
83    }
84}
85
86impl Default for MouseCaptureState {
87    fn default() -> Self {
88        Self::enabled()
89    }
90}
91
92/// Enable mouse capture
93///
94/// Sends the crossterm EnableMouseCapture command to the terminal.
95///
96/// # Errors
97/// Returns an error if the command fails to execute.
98pub fn enable_mouse_capture<W: Write>(writer: &mut W) -> io::Result<()> {
99    execute!(writer, EnableMouseCapture)
100}
101
102/// Disable mouse capture
103///
104/// Sends the crossterm DisableMouseCapture command to the terminal.
105/// When disabled, the terminal allows native text selection.
106///
107/// # Errors
108/// Returns an error if the command fails to execute.
109pub fn disable_mouse_capture<W: Write>(writer: &mut W) -> io::Result<()> {
110    execute!(writer, DisableMouseCapture)
111}
112
113/// Toggle mouse capture and update state
114///
115/// Toggles between enabled and disabled mouse capture. When disabled,
116/// the terminal allows native text selection (copy mode).
117///
118/// # Arguments
119/// * `writer` - The terminal output writer
120/// * `state` - The mouse capture state to update
121///
122/// # Returns
123/// Ok(true) if capture is now enabled, Ok(false) if disabled (copy mode)
124///
125/// # Errors
126/// Returns an error if the terminal command fails.
127pub fn toggle_mouse_capture<W: Write>(
128    writer: &mut W,
129    state: &mut MouseCaptureState,
130) -> io::Result<bool> {
131    let new_enabled = state.toggle();
132    if new_enabled {
133        enable_mouse_capture(writer)?;
134    } else {
135        disable_mouse_capture(writer)?;
136    }
137    Ok(new_enabled)
138}
139
140/// Set mouse capture to a specific state
141///
142/// # Arguments
143/// * `writer` - The terminal output writer
144/// * `state` - The mouse capture state to update
145/// * `enabled` - Whether to enable (true) or disable (false) capture
146///
147/// # Errors
148/// Returns an error if the terminal command fails.
149pub fn set_mouse_capture<W: Write>(
150    writer: &mut W,
151    state: &mut MouseCaptureState,
152    enabled: bool,
153) -> io::Result<()> {
154    if state.is_enabled() != enabled {
155        state.set_enabled(enabled);
156        if enabled {
157            enable_mouse_capture(writer)?;
158        } else {
159            disable_mouse_capture(writer)?;
160        }
161    }
162    Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_mouse_capture_state_new() {
171        let enabled = MouseCaptureState::new(true);
172        assert!(enabled.is_enabled());
173        assert!(!enabled.is_copy_mode());
174
175        let disabled = MouseCaptureState::new(false);
176        assert!(!disabled.is_enabled());
177        assert!(disabled.is_copy_mode());
178    }
179
180    #[test]
181    fn test_mouse_capture_state_constructors() {
182        let enabled = MouseCaptureState::enabled();
183        assert!(enabled.is_enabled());
184
185        let disabled = MouseCaptureState::disabled();
186        assert!(!disabled.is_enabled());
187    }
188
189    #[test]
190    fn test_mouse_capture_state_default() {
191        let state = MouseCaptureState::default();
192        assert!(state.is_enabled());
193    }
194
195    #[test]
196    fn test_mouse_capture_state_toggle() {
197        let mut state = MouseCaptureState::enabled();
198        assert!(state.is_enabled());
199
200        let result = state.toggle();
201        assert!(!result);
202        assert!(!state.is_enabled());
203        assert!(state.is_copy_mode());
204
205        let result = state.toggle();
206        assert!(result);
207        assert!(state.is_enabled());
208    }
209
210    #[test]
211    fn test_mouse_capture_state_set_enabled() {
212        let mut state = MouseCaptureState::enabled();
213
214        state.set_enabled(false);
215        assert!(!state.is_enabled());
216
217        state.set_enabled(true);
218        assert!(state.is_enabled());
219    }
220
221    #[test]
222    fn test_enable_mouse_capture() {
223        let mut buffer = Vec::new();
224        enable_mouse_capture(&mut buffer).unwrap();
225        // The buffer should contain escape sequences
226        assert!(!buffer.is_empty());
227    }
228
229    #[test]
230    fn test_disable_mouse_capture() {
231        let mut buffer = Vec::new();
232        disable_mouse_capture(&mut buffer).unwrap();
233        // The buffer should contain escape sequences
234        assert!(!buffer.is_empty());
235    }
236
237    #[test]
238    fn test_toggle_mouse_capture() {
239        let mut buffer = Vec::new();
240        let mut state = MouseCaptureState::enabled();
241
242        // Toggle to disabled
243        let result = toggle_mouse_capture(&mut buffer, &mut state).unwrap();
244        assert!(!result);
245        assert!(state.is_copy_mode());
246
247        // Toggle to enabled
248        buffer.clear();
249        let result = toggle_mouse_capture(&mut buffer, &mut state).unwrap();
250        assert!(result);
251        assert!(state.is_enabled());
252    }
253
254    #[test]
255    fn test_set_mouse_capture() {
256        let mut buffer = Vec::new();
257        let mut state = MouseCaptureState::enabled();
258
259        // Setting to same value should not write anything
260        set_mouse_capture(&mut buffer, &mut state, true).unwrap();
261        assert!(buffer.is_empty());
262
263        // Setting to different value should write
264        set_mouse_capture(&mut buffer, &mut state, false).unwrap();
265        assert!(!buffer.is_empty());
266        assert!(state.is_copy_mode());
267    }
268}