-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.rs
More file actions
374 lines (342 loc) · 10.6 KB
/
Copy pathmain.rs
File metadata and controls
374 lines (342 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
//! # RustyDiff - Main Entry Point
//!
//! This module serves as the entry point for the RustyDiff application. It handles:
//! - Command-line argument parsing
//! - Terminal setup and teardown
//! - Main application event loop orchestration
//! - Error handling and graceful shutdown
//!
//! ## Application Flow
//!
//! ```text
//! 1. Parse Command Line Arguments
//! └─ Extract file1 and file2 paths
//!
//! 2. Execute Diff Command
//! └─ diff::utils::run_diff()
//! ├─ Try git diff --no-index
//! └─ Fall back to diff -u
//!
//! 3. Parse Diff Output
//! └─ diff::parser::parse_unified_diff()
//! └─ Create structured DiffResult
//!
//! 4. Check for Changes
//! ├─ If identical: Print message and exit
//! └─ If different: Continue to UI
//!
//! 5. Setup Terminal
//! ├─ Enable raw mode (capture all keys)
//! └─ Enter alternate screen (use full terminal)
//!
//! 6. Create Application State
//! ├─ Initialize App with DiffResult
//! ├─ Load syntax definitions
//! └─ Load color theme
//!
//! 7. Run Event Loop
//! ├─ Render current frame
//! └─ Handle keyboard input
//!
//! 8. Restore Terminal
//! ├─ Disable raw mode
//! └─ Leave alternate screen
//!
//! 9. Exit with Result
//! ```
//!
//! ## Terminal Lifecycle
//!
//! ### Setup Phase
//! - `enable_raw_mode()`: Disable line buffering, capture all input
//! - `EnterAlternateScreen`: Switch to alternate terminal buffer
//! - Benefits: Full control over terminal, prevents output pollution
//!
//! ### Event Loop Phase
//! - Ratatui renders to alternate screen
//! - Terminal remains in raw mode for responsive input
//! - User can scroll, navigate, jump hunks
//!
//! ### Restore Phase
//! - `LeaveAlternateScreen`: Return to normal terminal
//! - `disable_raw_mode()`: Restore normal line buffering
//! - Ensures user's terminal state is not corrupted
//!
//! ## Error Handling Strategy
//!
//! Errors are handled at each stage:
//!
//! 1. **Argument Parsing**: Handled by clap
//! 2. **File Diff Execution**: Returns `anyhow::Result`
//! 3. **Diff Parsing**: Returns `anyhow::Result`
//! 4. **Terminal Setup**: Returns `anyhow::Result`, exits if fails
//! 5. **Rendering/Events**: Errors are logged but loop continues
//! 6. **Terminal Restore**: Attempts restore even if earlier error
//!
//! ## Key Design Decisions
//!
//! ### Identical File Handling
//! If files are identical, the application exits early with a message
//! rather than launching the UI. This saves resources and provides
//! quick feedback to the user.
//!
//! ### Terminal Setup Before Event Loop
//! Terminal setup happens before the event loop to ensure that any
//! setup errors prevent UI launch. This prevents a corrupted terminal
//! state if something goes wrong.
//!
//! ### Graceful Restore
//! The terminal is restored using a separate function that's called
//! in a finally-like pattern, ensuring restoration even if the event
//! loop encounters errors.
use anyhow::Result;
use clap::{Arg, command};
use crossterm::{
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
/// Re-export main modules at crate level for convenience
use rustydiff::diff;
use rustydiff::ui;
/// Main entry point for the RustyDiff application.
///
/// # Process
///
/// 1. Parses command-line arguments (file1 and file2)
/// 2. Executes diff command on the two files
/// 3. Parses the diff output into a structured format
/// 4. Checks if files are identical (exits early if so)
/// 5. Sets up the terminal for TUI mode
/// 6. Runs the main application event loop
/// 7. Restores the terminal to its original state
///
/// # Arguments
///
/// The application expects exactly two file paths:
/// ```bash
/// rustydiff <FILE1> <FILE2>
/// ```
///
/// # Returns
///
/// - `Ok(())` if the application completed successfully
/// - `Err` if any stage encounters an error
///
/// # Example
///
/// ```bash
/// $ rustydiff src/main.rs.old src/main.rs
/// ```
///
/// This compares two versions of main.rs and opens an interactive viewer.
fn main() -> Result<()> {
// =====================
// Argument Parsing
// =====================
let matches = command!()
.about("A Rust-based frontend for diff command.")
.arg(
Arg::new("file1")
.required(true)
.help("First file to compare"),
)
.arg(
Arg::new("file2")
.required(true)
.help("Second file to compare"),
)
.get_matches();
let file1 = matches
.get_one::<String>("file1")
.expect("file1 is required");
let file2 = matches
.get_one::<String>("file2")
.expect("file1 is required");
// =====================
// Execute Diff
// =====================
// Run the diff command (tries git diff first, falls back to standard diff)
let diff_output = diff::utils::run_diff(file1, file2)?;
// =====================
// Parse Diff Output
// =====================
// Parse the unified diff format into structured data
let diff_result = diff::parser::parse_unified_diff(diff_output)?;
// =====================
// Check for Changes
// =====================
// If files are identical, exit early with a message
if !diff_result.has_changes() {
println!("Files are identical!");
return Ok(());
}
// =====================
// Setup Terminal
// =====================
// Configure terminal for TUI rendering
setup_terminal()?;
// =====================
// Run Application
// =====================
// Run the main event loop (render + event handling)
let result = run_app(diff_result);
// =====================
// Restore Terminal
// =====================
// Always restore terminal, even if the app encountered errors
restore_terminal()?;
result
}
/// Configures the terminal for TUI rendering.
///
/// This function prepares the terminal for interactive use by:
/// 1. Enabling raw mode - disables line buffering and captures all keyboard input
/// 2. Switching to alternate screen - prevents terminal history pollution
///
/// # Returns
///
/// - `Ok(())` if terminal setup succeeded
/// - `Err` if raw mode or screen switching fails
///
/// # Raw Mode
///
/// Raw mode disables:
/// - Line buffering (input available immediately)
/// - Echo (keys not printed to screen)
/// - Signal handling (Ctrl+C doesn't send SIGINT)
///
/// Enables:
/// - Direct access to keyboard input (all keys)
/// - Mouse input (if supported)
/// - Full control over terminal state
///
/// # Alternate Screen
///
/// Alternate screen buffer:
/// - Preserves user's terminal history and scrollback
/// - Provides a clean canvas for the TUI
/// - Automatically restores normal screen on exit
///
/// # Example
///
/// ```ignore
/// setup_terminal()?;
/// // Terminal is now ready for TUI
/// ```
fn setup_terminal() -> Result<()> {
// Enable raw mode to capture all keyboard input
enable_raw_mode()?;
// Switch to alternate screen buffer
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
Ok(())
}
/// Restores the terminal to its original state.
///
/// This function reverses the changes made by `setup_terminal()`:
/// 1. Disables raw mode - restores normal input handling
/// 2. Leaves alternate screen - returns to normal terminal buffer
///
/// # Returns
///
/// - `Ok(())` if restoration succeeded
/// - `Err` if restoration fails
///
/// # Important
///
/// This function should always be called after the application finishes,
/// even if errors occurred. Failing to restore can leave the terminal
/// in an unusable state (invisible input, corrupted display).
///
/// # Example
///
/// ```ignore
/// setup_terminal()?;
/// let result = run_app(diff_result);
/// restore_terminal()?;
/// result
/// ```
fn restore_terminal() -> Result<()> {
// Disable raw mode to restore normal input handling
disable_raw_mode()?;
// Return to normal screen buffer
let mut stdout = io::stdout();
execute!(stdout, LeaveAlternateScreen)?;
Ok(())
}
/// Runs the main application event loop.
///
/// This is the core of the application. It:
/// 1. Creates the terminal backend
/// 2. Initializes the application state with the diff result
/// 3. Loops until the user quits:
/// - Renders the current frame (using renderers::render)
/// - Handles keyboard input (using events::handle_events)
/// - Checks the quit flag to exit loop
///
/// # Arguments
///
/// * `diff_result` - The parsed diff data to display and navigate
///
/// # Returns
///
/// - `Ok(())` if the event loop completed successfully (user quit)
/// - `Err` if a render or event handling error occurs
///
/// # Event Loop Pattern
///
/// ```text
/// while !app.should_quit {
/// terminal.draw(|frame| render(frame, &mut app))?;
/// events::handle_events(&mut app)?;
/// }
/// ```
///
/// This pattern ensures:
/// - UI is always up-to-date (re-renders every frame)
/// - Input is responsive (checked every iteration)
/// - Quit signal is respected (loop exits when flag is set)
///
/// # Frame Rate
///
/// The application renders at the monitor's refresh rate (typically 60 Hz).
/// Event handling uses a 100ms timeout, so the UI remains responsive even
/// when no events occur.
///
/// # Example
///
/// ```ignore
/// let diff_result = parse_diff()?;
/// run_app(diff_result)? // Runs until user quits
/// ```
fn run_app(diff_result: diff::models::DiffResult) -> Result<()> {
// =====================
// Create Terminal
// =====================
// Create the terminal backend using crossterm
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
// =====================
// Initialize App State
// =====================
// Create the application state with the diff result
// This loads the syntax set, theme, and initializes scroll position
let mut app = ui::app::App::new(diff_result);
// =====================
// Main Event Loop
// =====================
// Loop until the user requests to quit
while !app.should_quit {
// Render the current frame
terminal.draw(|frame| {
// Rendering errors are non-fatal, just skip this frame
let _ = ui::renderers::render(frame, &mut app);
})?;
// Handle keyboard input (non-blocking with 100ms timeout)
// This updates the app state (scroll, navigation, quit flag)
ui::events::handle_events(&mut app)?;
}
Ok(())
}