Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ztroop committed Apr 2, 2024
1 parent 69382ea commit b34854e
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 286 deletions.
92 changes: 92 additions & 0 deletions src/app.rs
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;
}
}
65 changes: 65 additions & 0 deletions src/event.rs
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()
}
}
21 changes: 21 additions & 0 deletions src/handler.rs
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(())
}
35 changes: 31 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
use std::io;

use app::App;
use event::{Event, EventHandler};
use file::{diff_files, read_file};
use viewer::FileDiffViewer;
use handler::handle_key_events;
use ratatui::{backend::CrosstermBackend, Terminal};
use tui::Tui;

mod app;
mod event;
mod file;
mod viewer;
mod handler;
mod tui;
mod ui;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
Expand All @@ -15,6 +25,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let file2_data = read_file(&args[2])?;
let diffs = diff_files(&file1_data, &file2_data);

let mut viewer = FileDiffViewer::new(diffs);
viewer.run()
let backend = CrosstermBackend::new(io::stderr());
let terminal = Terminal::new(backend)?;
let events = EventHandler::new(1_000);
let mut tui = Tui::new(terminal, events);
tui.init()?;

let mut app = App::new(file1_data, file2_data, diffs);
while app.running {
tui.draw(&mut app)?;
match tui.events.next()? {
Event::Tick => app.tick()?,
Event::Key(key_event) => handle_key_events(key_event, &mut app, tui.size())?,
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
}
}

tui.exit()?;
Ok(())
}
83 changes: 83 additions & 0 deletions src/tui.rs
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(())
}
}
Loading

0 comments on commit b34854e

Please sign in to comment.