use std::io;
use std::time::Duration;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Tabs},
};
use ratatui_interact::components::{
DiffData, DiffViewer, DiffViewerState, DiffViewerStyle, handle_diff_viewer_key,
handle_diff_viewer_mouse,
};
use ratatui_interact::events::is_close_key;
const BASIC_DIFF: &str = r#"--- a/greeting.txt
+++ b/greeting.txt
@@ -1,4 +1,5 @@
Hello World
-This is old text
+This is new text
+And an additional line
Goodbye
The End
"#;
const CODE_DIFF: &str = r#"--- a/src/main.rs
+++ b/src/main.rs
@@ -1,10 +1,12 @@
fn main() {
- println!("Hello, world!");
+ println!("Hello, Rust!");
+
+ // New feature: process arguments
let args: Vec<String> = std::env::args().collect();
- if args.len() > 1 {
- println!("Args: {:?}", args);
+ for arg in &args[1..] {
+ println!("Processing: {}", arg);
}
}
@@ -15,6 +17,10 @@ fn helper_function() {
let x = 42;
let y = x * 2;
- println!("Result: {}", y);
+ let result = calculate(x, y);
+ println!("Result: {}", result);
+}
+
+fn calculate(a: i32, b: i32) -> i32 {
+ a + b
}
"#;
const LARGE_DIFF: &str = r#"--- a/config.json
+++ b/config.json
@@ -1,20 +1,25 @@
{
"name": "my-app",
- "version": "1.0.0",
+ "version": "2.0.0",
"description": "A sample application",
+ "author": "Developer",
+ "license": "MIT",
"main": "index.js",
"scripts": {
"start": "node index.js",
- "test": "jest"
+ "test": "jest --coverage",
+ "lint": "eslint src/",
+ "build": "tsc"
},
"dependencies": {
- "express": "^4.17.1",
- "lodash": "^4.17.21"
+ "express": "^4.18.2",
+ "lodash": "^4.17.21",
+ "axios": "^1.4.0"
},
"devDependencies": {
- "jest": "^27.0.0"
+ "jest": "^29.0.0",
+ "typescript": "^5.0.0",
+ "eslint": "^8.40.0"
}
}
"#;
struct App {
diff_states: Vec<DiffViewerState>,
selected_tab: usize,
tab_names: Vec<&'static str>,
should_quit: bool,
}
impl App {
fn new() -> Self {
let diffs = vec![
DiffData::from_unified_diff(BASIC_DIFF),
DiffData::from_unified_diff(CODE_DIFF),
DiffData::from_unified_diff(LARGE_DIFF),
];
let diff_states = diffs.into_iter().map(DiffViewerState::new).collect();
Self {
diff_states,
selected_tab: 0,
tab_names: vec!["Basic", "Code", "Config"],
should_quit: false,
}
}
fn current_state(&self) -> &DiffViewerState {
&self.diff_states[self.selected_tab]
}
fn current_state_mut(&mut self) -> &mut DiffViewerState {
&mut self.diff_states[self.selected_tab]
}
fn next_tab(&mut self) {
self.selected_tab = (self.selected_tab + 1) % self.tab_names.len();
}
fn prev_tab(&mut self) {
if self.selected_tab == 0 {
self.selected_tab = self.tab_names.len() - 1;
} else {
self.selected_tab -= 1;
}
}
}
fn main() -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new();
loop {
terminal.draw(|f| ui(f, &mut app))?;
if event::poll(Duration::from_millis(100))? {
match event::read()? {
Event::Key(key) => {
if is_close_key(&key) || key.code == KeyCode::Char('q') {
app.should_quit = true;
} else if key.code == KeyCode::Tab {
app.next_tab();
} else if key.code == KeyCode::BackTab {
app.prev_tab();
} else {
handle_diff_viewer_key(app.current_state_mut(), &key);
}
}
Event::Mouse(mouse) => {
handle_diff_viewer_mouse(app.current_state_mut(), &mouse);
}
_ => {}
}
}
if app.should_quit {
break;
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
fn ui(f: &mut Frame, app: &mut App) {
let area = f.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ])
.split(area);
render_header(f, app, chunks[0]);
let state = app.current_state();
let title = format!(
"{} - {}",
app.tab_names[app.selected_tab],
state.diff.old_path.as_deref().unwrap_or("unknown")
);
let inner_height = chunks[1].height.saturating_sub(4) as usize; let inner_width = chunks[1].width.saturating_sub(2) as usize;
app.current_state_mut().visible_height = inner_height;
app.current_state_mut().visible_width = inner_width;
let style = DiffViewerStyle::default();
let viewer = DiffViewer::new(app.current_state())
.title(&title)
.style(style)
.show_stats(true);
f.render_widget(viewer, chunks[1]);
render_help(f, chunks[2]);
}
fn render_header(f: &mut Frame, app: &App, area: Rect) {
let block = Block::default()
.title(" Diff Viewer Demo ")
.title_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
f.render_widget(block, area);
let tabs: Vec<Line> = app
.tab_names
.iter()
.enumerate()
.map(|(i, name)| {
let style = if i == app.selected_tab {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
Line::from(Span::styled(*name, style))
})
.collect();
let tabs_widget = Tabs::new(tabs)
.select(app.selected_tab)
.divider(" | ")
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
f.render_widget(tabs_widget, inner);
}
fn render_help(f: &mut Frame, area: Rect) {
let help_text = vec![
Line::from(vec![
Span::styled("Tab/Shift+Tab", Style::default().fg(Color::Yellow)),
Span::raw(": switch diff "),
Span::styled("j/k/↑/↓", Style::default().fg(Color::Yellow)),
Span::raw(": scroll "),
Span::styled("]/[", Style::default().fg(Color::Yellow)),
Span::raw(": next/prev hunk "),
Span::styled("n/N", Style::default().fg(Color::Yellow)),
Span::raw(": next/prev change "),
]),
Line::from(vec![
Span::styled("v", Style::default().fg(Color::Yellow)),
Span::raw(": toggle view mode "),
Span::styled("g/G", Style::default().fg(Color::Yellow)),
Span::raw(": top/bottom "),
Span::styled("/", Style::default().fg(Color::Yellow)),
Span::raw(": search "),
Span::styled("q/Esc", Style::default().fg(Color::Yellow)),
Span::raw(": quit"),
]),
];
let help = Paragraph::new(help_text)
.block(Block::default().borders(Borders::TOP))
.style(Style::default().fg(Color::Gray));
f.render_widget(help, area);
}