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}