Skip to main content

ratatui_interact/utils/
view_copy.rs

1//! View/Copy mode and exit strategy utilities
2//!
3//! Provides functionality for:
4//! - "View/Copy mode" that exits the alternate screen for native text selection
5//! - Exit strategies: restore original console or print content
6//!
7//! # View/Copy Mode Example
8//!
9//! ```rust,ignore
10//! use ratatui_interact::utils::{ViewCopyMode, ViewCopyConfig};
11//!
12//! let config = ViewCopyConfig::default()
13//!     .with_header("My Content")
14//!     .show_hints(true);
15//!
16//! let mode = ViewCopyMode::enter_with_config(&mut stdout, config)?;
17//! mode.print_lines(&content_lines)?;
18//!
19//! loop {
20//!     match mode.wait_for_input()? {
21//!         ViewCopyAction::Exit => break,
22//!         ViewCopyAction::ToggleLineNumbers => {
23//!             mode.clear()?;
24//!             mode.print_lines(&new_content)?;
25//!         }
26//!         ViewCopyAction::None => {}
27//!     }
28//! }
29//!
30//! mode.exit(&mut terminal)?;
31//! ```
32//!
33//! # Exit Strategy Example
34//!
35//! ```rust,ignore
36//! use ratatui_interact::utils::ExitStrategy;
37//!
38//! // At app exit, choose strategy:
39//! let strategy = ExitStrategy::PrintContent(content_lines);
40//! // or: let strategy = ExitStrategy::RestoreConsole;
41//!
42//! strategy.execute()?;
43//! ```
44
45use std::io::{self, Write};
46
47use crossterm::{
48    cursor::MoveTo,
49    event::{self, Event, KeyCode},
50    execute,
51    terminal::{
52        Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen,
53        disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap,
54    },
55};
56
57/// Action returned from waiting for input in View/Copy mode
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ViewCopyAction {
60    /// User wants to exit view/copy mode
61    Exit,
62    /// User wants to toggle line numbers
63    ToggleLineNumbers,
64    /// No action (continue waiting)
65    None,
66}
67
68/// Configuration for View/Copy mode
69#[derive(Debug, Clone)]
70pub struct ViewCopyConfig {
71    /// Header text to show at the top
72    pub header: Option<String>,
73    /// Whether to show keyboard hints
74    pub show_hints: bool,
75    /// Exit keys (default: 'c', 'q', Esc)
76    pub exit_keys: Vec<KeyCode>,
77    /// Toggle line numbers key (default: 'n')
78    pub toggle_key: KeyCode,
79}
80
81impl Default for ViewCopyConfig {
82    fn default() -> Self {
83        Self {
84            header: None,
85            show_hints: true,
86            exit_keys: vec![KeyCode::Char('c'), KeyCode::Char('q'), KeyCode::Esc],
87            toggle_key: KeyCode::Char('n'),
88        }
89    }
90}
91
92impl ViewCopyConfig {
93    /// Set the header text
94    pub fn with_header(mut self, header: impl Into<String>) -> Self {
95        self.header = Some(header.into());
96        self
97    }
98
99    /// Set whether to show keyboard hints
100    pub fn show_hints(mut self, show: bool) -> Self {
101        self.show_hints = show;
102        self
103    }
104
105    /// Set custom exit keys
106    pub fn exit_keys(mut self, keys: Vec<KeyCode>) -> Self {
107        self.exit_keys = keys;
108        self
109    }
110
111    /// Set the toggle line numbers key
112    pub fn toggle_key(mut self, key: KeyCode) -> Self {
113        self.toggle_key = key;
114        self
115    }
116}
117
118/// Handle for View/Copy mode
119///
120/// Created by `ViewCopyMode::enter()`, must call `exit()` when done.
121pub struct ViewCopyMode {
122    config: ViewCopyConfig,
123}
124
125impl ViewCopyMode {
126    /// Enter View/Copy mode
127    ///
128    /// This will:
129    /// 1. Leave the alternate screen
130    /// 2. Disable mouse capture
131    /// 3. Clear the screen and scrollback buffer
132    /// 4. Disable raw mode (so println works normally)
133    pub fn enter<W: Write>(stdout: &mut W) -> io::Result<Self> {
134        Self::enter_with_config(stdout, ViewCopyConfig::default())
135    }
136
137    /// Enter View/Copy mode with custom configuration
138    pub fn enter_with_config<W: Write>(
139        stdout: &mut W,
140        config: ViewCopyConfig,
141    ) -> io::Result<Self> {
142        use crossterm::event::DisableMouseCapture;
143
144        // Leave alternate screen and disable mouse capture
145        execute!(stdout, LeaveAlternateScreen, DisableMouseCapture)?;
146
147        // Disable raw mode so println works
148        disable_raw_mode()?;
149
150        // Clear screen and scrollback buffer
151        execute!(
152            stdout,
153            Clear(ClearType::Purge),
154            Clear(ClearType::All),
155            MoveTo(0, 0),
156            DisableLineWrap
157        )?;
158        stdout.flush()?;
159
160        Ok(Self { config })
161    }
162
163    /// Clear the screen (for reprinting content)
164    pub fn clear(&self) -> io::Result<()> {
165        let mut stdout = io::stdout();
166        execute!(
167            stdout,
168            Clear(ClearType::Purge),
169            Clear(ClearType::All),
170            MoveTo(0, 0)
171        )?;
172        stdout.flush()?;
173        Ok(())
174    }
175
176    /// Print lines to stdout with optional header and hints
177    pub fn print_lines(&self, lines: &[String]) -> io::Result<()> {
178        if self.config.show_hints {
179            if let Some(header) = &self.config.header {
180                println!("=== {} ===", header);
181            } else {
182                println!("=== View/Copy Mode ===");
183            }
184            println!("Press 'c', 'q', or Esc to exit | 'n' to toggle line numbers");
185            println!("{}", "─".repeat(60));
186            println!();
187        }
188
189        for line in lines {
190            println!("{}", line);
191        }
192
193        if self.config.show_hints {
194            println!();
195            println!("{}", "─".repeat(60));
196            println!("Press 'c', 'q', or Esc to exit | 'n' to toggle line numbers");
197        }
198
199        io::stdout().flush()?;
200        Ok(())
201    }
202
203    /// Print raw lines without any formatting
204    pub fn print_raw(&self, lines: &[String]) -> io::Result<()> {
205        for line in lines {
206            println!("{}", line);
207        }
208        io::stdout().flush()?;
209        Ok(())
210    }
211
212    /// Wait for user input and return the action
213    ///
214    /// Note: This temporarily enables raw mode to catch keypresses,
215    /// then disables it again so subsequent prints work.
216    pub fn wait_for_input(&self) -> io::Result<ViewCopyAction> {
217        // Enable raw mode to catch keys
218        enable_raw_mode()?;
219
220        let action = loop {
221            if event::poll(std::time::Duration::from_millis(100))? {
222                if let Event::Key(key) = event::read()? {
223                    if self.config.exit_keys.contains(&key.code) {
224                        break ViewCopyAction::Exit;
225                    } else if key.code == self.config.toggle_key {
226                        break ViewCopyAction::ToggleLineNumbers;
227                    }
228                }
229            }
230        };
231
232        // Disable raw mode for any subsequent prints
233        disable_raw_mode()?;
234
235        Ok(action)
236    }
237
238    /// Exit View/Copy mode and return to the TUI
239    ///
240    /// This will:
241    /// 1. Re-enable raw mode
242    /// 2. Re-enter the alternate screen
243    /// 3. Re-enable mouse capture
244    /// 4. Clear the terminal to force a full redraw
245    pub fn exit<B>(self, terminal: &mut ratatui::Terminal<B>) -> io::Result<()>
246    where
247        B: ratatui::backend::Backend,
248        io::Error: From<B::Error>,
249    {
250        use crossterm::event::EnableMouseCapture;
251
252        let mut stdout = io::stdout();
253
254        // Re-enable raw mode
255        enable_raw_mode()?;
256
257        // Re-enter alternate screen and enable mouse capture
258        execute!(stdout, EnableLineWrap, EnterAlternateScreen, EnableMouseCapture)?;
259
260        // Clear terminal to force full redraw
261        terminal.clear()?;
262
263        Ok(())
264    }
265}
266
267/// Clear the main screen buffer before entering alternate screen
268///
269/// Call this at app startup to ensure View/Copy mode has a clean buffer.
270/// This prevents old terminal content from appearing when leaving alternate screen.
271///
272/// **Note:** If you want to support `ExitStrategy::RestoreConsole`, do NOT call this
273/// function at startup, as it will clear the original terminal content.
274pub fn clear_main_screen() -> io::Result<()> {
275    let mut stdout = io::stdout();
276    execute!(
277        stdout,
278        Clear(ClearType::Purge),
279        Clear(ClearType::All),
280        MoveTo(0, 0)
281    )?;
282    stdout.flush()?;
283    Ok(())
284}
285
286/// Strategy for exiting the application
287#[derive(Debug, Clone)]
288pub enum ExitStrategy {
289    /// Restore the original terminal content
290    ///
291    /// Simply exits the alternate screen without printing anything.
292    /// The terminal will show whatever was displayed before the app started.
293    RestoreConsole,
294
295    /// Print content to stdout on exit
296    ///
297    /// Clears the screen and prints the provided lines.
298    PrintContent(Vec<String>),
299}
300
301impl ExitStrategy {
302    /// Execute the exit strategy
303    ///
304    /// This should be called after:
305    /// 1. Disabling raw mode
306    /// 2. Leaving alternate screen
307    /// 3. Disabling mouse capture
308    ///
309    /// It handles the final output based on the chosen strategy.
310    pub fn execute(&self) -> io::Result<()> {
311        match self {
312            ExitStrategy::RestoreConsole => {
313                // Nothing to do - the terminal already restored the original content
314                // when we left the alternate screen
315                Ok(())
316            }
317            ExitStrategy::PrintContent(lines) => {
318                let mut stdout = io::stdout();
319                // Clear screen and scrollback to remove any artifacts
320                execute!(
321                    stdout,
322                    Clear(ClearType::Purge),
323                    Clear(ClearType::All),
324                    MoveTo(0, 0)
325                )?;
326                // Print the content
327                for line in lines {
328                    println!("{}", line);
329                }
330                stdout.flush()?;
331                Ok(())
332            }
333        }
334    }
335
336    /// Create a PrintContent strategy from a slice of strings
337    pub fn print_content(lines: &[String]) -> Self {
338        ExitStrategy::PrintContent(lines.to_vec())
339    }
340
341    /// Create a PrintContent strategy from an iterator
342    pub fn print_content_iter<I, S>(lines: I) -> Self
343    where
344        I: IntoIterator<Item = S>,
345        S: Into<String>,
346    {
347        ExitStrategy::PrintContent(lines.into_iter().map(|s| s.into()).collect())
348    }
349}