-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
418 additions
and
286 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
use std::error; | ||
|
||
pub struct App { | ||
pub running: bool, | ||
pub file1_data: Vec<u8>, | ||
pub file2_data: Vec<u8>, | ||
pub diffs: Vec<(usize, u8)>, | ||
pub cursor_pos: usize, | ||
pub scroll: usize, | ||
pub bytes_per_line: usize, | ||
} | ||
|
||
impl App { | ||
pub fn new(file1_data: Vec<u8>, file2_data: Vec<u8>, diffs: Vec<(usize, u8)>) -> Self { | ||
Self { | ||
running: true, | ||
file1_data, | ||
file2_data, | ||
diffs, | ||
cursor_pos: 0, | ||
scroll: 0, | ||
bytes_per_line: 0, | ||
} | ||
} | ||
|
||
pub fn tick(&mut self) -> Result<(), Box<dyn error::Error>> { | ||
Ok(()) | ||
} | ||
|
||
pub fn move_cursor_down(&mut self, terminal_height: u16) { | ||
let lines = (terminal_height - 5) as usize; | ||
let max_cursor_pos = self.diffs.len().saturating_sub(1); | ||
|
||
// Increment cursor position if not at the end of diffs | ||
if self.cursor_pos < max_cursor_pos { | ||
self.cursor_pos += self.bytes_per_line; | ||
self.cursor_pos = self.cursor_pos.min(max_cursor_pos); | ||
} | ||
|
||
// Adjust scrolling if cursor moves beyond the visible area | ||
if (self.cursor_pos / self.bytes_per_line) >= (self.scroll + lines) | ||
&& (self.scroll + lines) | ||
< ((self.diffs.len() + self.bytes_per_line - 1) / self.bytes_per_line) | ||
{ | ||
self.scroll += 1; | ||
} | ||
} | ||
|
||
pub fn move_cursor_up(&mut self) { | ||
if self.cursor_pos >= self.bytes_per_line { | ||
self.cursor_pos = self.cursor_pos.saturating_sub(self.bytes_per_line); | ||
} | ||
|
||
// Adjust scrolling if cursor moves above the visible area | ||
if self.cursor_pos / self.bytes_per_line < self.scroll { | ||
self.scroll = self.scroll.saturating_sub(1); | ||
} | ||
} | ||
|
||
pub fn move_cursor_right(&mut self, terminal_height: u16) { | ||
let lines = (terminal_height - 5) as usize; | ||
let max_cursor_pos = self.diffs.len().saturating_sub(1); | ||
|
||
// Move cursor right if not at the end of diffs | ||
if self.cursor_pos < max_cursor_pos { | ||
self.cursor_pos += 1; | ||
} | ||
|
||
// Special handling for bottom right movement | ||
let cursor_line = self.cursor_pos / self.bytes_per_line; | ||
if cursor_line >= self.scroll + lines { | ||
self.scroll = cursor_line + 1 - lines; | ||
} | ||
} | ||
|
||
pub fn move_cursor_left(&mut self) { | ||
// Move cursor left if not at the start | ||
if self.cursor_pos > 0 { | ||
self.cursor_pos -= 1; | ||
} | ||
|
||
// Special handling for top left movement | ||
let cursor_line = self.cursor_pos / self.bytes_per_line; | ||
if cursor_line < self.scroll { | ||
self.scroll = cursor_line; | ||
} | ||
} | ||
|
||
pub fn quit(&mut self) { | ||
self.running = false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
use crossterm::event::{self, Event as CEvent, KeyEvent, MouseEvent}; | ||
use std::sync::mpsc; | ||
use std::thread; | ||
use std::time::{Duration, Instant}; | ||
|
||
#[derive(Clone, Copy, Debug)] | ||
pub enum Event { | ||
Tick, | ||
Key(KeyEvent), | ||
Mouse(MouseEvent), | ||
Resize(u16, u16), | ||
} | ||
|
||
#[allow(dead_code)] | ||
#[derive(Debug)] | ||
pub struct EventHandler { | ||
sender: mpsc::Sender<Event>, | ||
receiver: mpsc::Receiver<Event>, | ||
handler: thread::JoinHandle<()>, | ||
} | ||
|
||
impl EventHandler { | ||
pub fn new(tick_rate: u64) -> Self { | ||
let tick_rate = Duration::from_millis(tick_rate); | ||
let (sender, receiver) = mpsc::channel(); | ||
let handler = { | ||
let sender = sender.clone(); | ||
thread::spawn(move || { | ||
let mut last_tick = Instant::now(); | ||
loop { | ||
let timeout = tick_rate | ||
.checked_sub(last_tick.elapsed()) | ||
.unwrap_or(tick_rate); | ||
|
||
if event::poll(timeout).expect("Failed to poll new events") { | ||
match event::read().expect("Unable to read event") { | ||
CEvent::Key(e) => sender.send(Event::Key(e)), | ||
CEvent::Mouse(e) => sender.send(Event::Mouse(e)), | ||
CEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), | ||
CEvent::FocusGained => Ok(()), | ||
CEvent::FocusLost => Ok(()), | ||
CEvent::Paste(_) => unimplemented!(), | ||
} | ||
.expect("Failed to send terminal event") | ||
} | ||
|
||
if last_tick.elapsed() >= tick_rate { | ||
sender.send(Event::Tick).expect("Failed to send tick event"); | ||
last_tick = Instant::now(); | ||
} | ||
} | ||
}) | ||
}; | ||
Self { | ||
sender, | ||
receiver, | ||
handler, | ||
} | ||
} | ||
|
||
/// Receive the next event from the handler thread. | ||
pub fn next(&self) -> Result<Event, mpsc::RecvError> { | ||
self.receiver.recv() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
use crossterm::event::{KeyCode, KeyEvent}; | ||
|
||
use crate::{app::App, tui::TerminalSize}; | ||
|
||
pub fn handle_key_events( | ||
key_event: KeyEvent, | ||
app: &mut App, | ||
size: TerminalSize, | ||
) -> Result<(), Box<dyn std::error::Error>> { | ||
match key_event.code { | ||
KeyCode::Char('q') => { | ||
app.quit(); | ||
} | ||
KeyCode::Down | KeyCode::Char('j') => app.move_cursor_down(size.height), | ||
KeyCode::Up | KeyCode::Char('k') => app.move_cursor_up(), | ||
KeyCode::Right | KeyCode::Char('l') => app.move_cursor_right(size.height), | ||
KeyCode::Left | KeyCode::Char('h') => app.move_cursor_left(), | ||
_ => {} | ||
} | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
use crate::app::App; | ||
use crate::event::EventHandler; | ||
use crate::ui; | ||
use crossterm::cursor::MoveTo; | ||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; | ||
use crossterm::terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; | ||
use ratatui::backend::Backend; | ||
use ratatui::Terminal; | ||
use std::io; | ||
use std::panic; | ||
|
||
/// Representation of a terminal user interface. | ||
#[derive(Debug)] | ||
pub struct Tui<B: Backend> { | ||
/// Interface to the Terminal. | ||
terminal: Terminal<B>, | ||
/// Terminal event handler. | ||
pub events: EventHandler, | ||
} | ||
|
||
pub struct TerminalSize { | ||
pub width: u16, | ||
pub height: u16, | ||
} | ||
|
||
impl<B: Backend> Tui<B> { | ||
/// Constructs a new instance of [`Tui`]. | ||
pub fn new(terminal: Terminal<B>, events: EventHandler) -> Self { | ||
Self { terminal, events } | ||
} | ||
|
||
/// Initializes the terminal interface. | ||
pub fn init(&mut self) -> Result<(), Box<dyn std::error::Error>> { | ||
terminal::enable_raw_mode()?; | ||
crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; | ||
|
||
// Define a custom panic hook to reset the terminal properties. | ||
let panic_hook = panic::take_hook(); | ||
panic::set_hook(Box::new(move |panic| { | ||
Self::reset().expect("failed to reset the terminal"); | ||
panic_hook(panic); | ||
})); | ||
|
||
self.terminal.hide_cursor()?; | ||
self.terminal.clear()?; | ||
Ok(()) | ||
} | ||
|
||
/// [`Draw`] the terminal interface by [`rendering`] the widgets. | ||
pub fn draw(&mut self, app: &mut App) -> Result<(), Box<dyn std::error::Error>> { | ||
self.terminal.draw(|frame| ui::render(app, frame))?; | ||
Ok(()) | ||
} | ||
|
||
/// Resets the terminal interface. | ||
fn reset() -> Result<(), Box<dyn std::error::Error>> { | ||
terminal::disable_raw_mode()?; | ||
crossterm::execute!( | ||
io::stderr(), | ||
LeaveAlternateScreen, | ||
DisableMouseCapture, | ||
Clear(ClearType::All), | ||
MoveTo(0, 0) | ||
)?; | ||
Ok(()) | ||
} | ||
|
||
/// Returns the size of the terminal interface. | ||
pub fn size(&self) -> TerminalSize { | ||
let size = self.terminal.size().unwrap(); | ||
TerminalSize { | ||
width: size.width, | ||
height: size.height, | ||
} | ||
} | ||
|
||
/// Exits the terminal interface. | ||
pub fn exit(&mut self) -> Result<(), Box<dyn std::error::Error>> { | ||
Self::reset()?; | ||
self.terminal.show_cursor()?; | ||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.