diffs-rs 0.1.1

Fast terminal diff viewer and embeddable Rust diff engine
Documentation
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, MouseButton, MouseEventKind};

use super::{App, Layout, sidebar::Focus};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputEffect {
    Ignored,
    Redraw,
    Highlight,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Command {
    Down,
    Up,
    PageDown,
    PageUp,
    FirstSource,
    LastSource,
    NextFile,
    PreviousFile,
    NextHunk,
    PreviousHunk,
    Left,
    Right,
    SwitchSide,
    ToggleLayout,
    ToggleWrap,
    ToggleNumbers,
    ToggleTheme,
    Files,
    Help,
    Back,
    Activate,
    ToggleFolder,
    TreeStart,
    TreeEnd,
    SelectLines,
    ClearSelection,
}

impl Command {
    pub fn from_key(key: KeyEvent) -> Option<Self> {
        if key.kind == KeyEventKind::Release {
            return None;
        }
        Some(match key.code {
            KeyCode::Down | KeyCode::Char('j') => Self::Down,
            KeyCode::Up | KeyCode::Char('k') => Self::Up,
            KeyCode::PageDown => Self::PageDown,
            KeyCode::PageUp => Self::PageUp,
            KeyCode::Char('g') => Self::FirstSource,
            KeyCode::Char('G') => Self::LastSource,
            KeyCode::Char('n') => Self::NextFile,
            KeyCode::Char('N') => Self::PreviousFile,
            KeyCode::Char(']') => Self::NextHunk,
            KeyCode::Char('[') => Self::PreviousHunk,
            KeyCode::Left => Self::Left,
            KeyCode::Right => Self::Right,
            KeyCode::Tab => Self::SwitchSide,
            KeyCode::Char('u') => Self::ToggleLayout,
            KeyCode::Char('w') => Self::ToggleWrap,
            KeyCode::Char('l') => Self::ToggleNumbers,
            KeyCode::Char('t') => Self::ToggleTheme,
            KeyCode::Char('f') => Self::Files,
            KeyCode::Char('?') => Self::Help,
            KeyCode::Esc => Self::Back,
            KeyCode::Enter => Self::Activate,
            KeyCode::Char(' ') => Self::ToggleFolder,
            KeyCode::Home => Self::TreeStart,
            KeyCode::End => Self::TreeEnd,
            _ => return None,
        })
    }
}

impl App {
    pub fn handle_event(&mut self, event: Event) -> anyhow::Result<InputEffect> {
        let mut highlight = true;
        match event {
            Event::Resize(w, h) => {
                self.prepare_view(ratatui::layout::Rect::new(0, 0, w, h))?;
            }
            Event::Key(key) => {
                return Command::from_key(key)
                    .map_or(Ok(InputEffect::Ignored), |command| self.execute(command));
            }
            Event::Mouse(mouse) if !self.help => {
                let point = ratatui::layout::Position::new(mouse.column, mouse.row);
                if self.sidebar.area.contains(point) {
                    highlight = false;
                    match mouse.kind {
                        MouseEventKind::ScrollDown => self.sidebar.scroll(3),
                        MouseEventKind::ScrollUp => self.sidebar.scroll(-3),
                        MouseEventKind::Down(MouseButton::Left) => {
                            self.sidebar.focus = Focus::Files;
                            if mouse.row > self.sidebar.area.y {
                                let index = self.sidebar.top
                                    + usize::from(mouse.row - self.sidebar.area.y - 1);
                                if let Some(row) = self.sidebar.tree.rows().get(index) {
                                    self.sidebar.cursor = Some(row.node);
                                    highlight = self.activate_tree();
                                }
                            }
                        }
                        _ => return Ok(InputEffect::Ignored),
                    }
                } else if self.sidebar.diff.contains(point) {
                    match mouse.kind {
                        MouseEventKind::ScrollDown => self.scroll(3)?,
                        MouseEventKind::ScrollUp => self.scroll(-3)?,
                        MouseEventKind::Down(MouseButton::Left) => {
                            self.sidebar.focus = Focus::Diff;
                            let y = mouse.row - self.sidebar.diff.y + 1;
                            if let Some((position, _)) =
                                self.visible.iter().find(|(_, row)| *row == y)
                            {
                                self.select(*position, mouse.column - self.sidebar.diff.x)?;
                                self.expand_context()?;
                            }
                        }
                        _ => return Ok(InputEffect::Ignored),
                    }
                } else {
                    return Ok(InputEffect::Ignored);
                }
            }
            _ => return Ok(InputEffect::Ignored),
        }
        Ok(if highlight {
            InputEffect::Highlight
        } else {
            InputEffect::Redraw
        })
    }

    pub fn execute(&mut self, command: Command) -> anyhow::Result<InputEffect> {
        self.sidebar.notice = None;
        let mut highlight = true;
        match command {
            Command::Help => {
                self.help = !self.help;
                highlight = false;
            }
            Command::Back if self.help => {
                self.help = false;
                highlight = false;
            }
            _ if self.help => return Ok(InputEffect::Ignored),
            Command::Files => self.files_command()?,
            command if self.sidebar.focused() => {
                highlight = false;
                match command {
                    Command::Back | Command::SwitchSide => self.sidebar.focus = Focus::Diff,
                    Command::Down => self.sidebar.move_cursor(1),
                    Command::Up => self.sidebar.move_cursor(-1),
                    Command::PageDown => self.sidebar.move_cursor(self.sidebar.page() as isize),
                    Command::PageUp => self.sidebar.move_cursor(-(self.sidebar.page() as isize)),
                    Command::TreeStart => self.sidebar.move_cursor(isize::MIN),
                    Command::TreeEnd => self.sidebar.move_cursor(isize::MAX),
                    Command::Right => self.sidebar.right(),
                    Command::Left => self.sidebar.left(),
                    Command::Activate => highlight = self.activate_tree(),
                    Command::ToggleFolder => {
                        if let Some(id) = self.sidebar.cursor {
                            self.sidebar.toggle(id);
                        }
                    }
                    _ => return Ok(InputEffect::Ignored),
                }
            }
            _ if self.sidebar.diff.is_empty() => return Ok(InputEffect::Ignored),
            Command::Down => self.move_cursor(1)?,
            Command::Up => self.move_cursor(-1)?,
            Command::PageDown => self.move_cursor(self.height.saturating_sub(4).max(1) as isize)?,
            Command::PageUp => {
                self.move_cursor(-(self.height.saturating_sub(4).max(1) as isize))?
            }
            Command::FirstSource => self.jump(false)?,
            Command::LastSource => self.jump(true)?,
            Command::NextHunk => self.jump_section(true, false),
            Command::PreviousHunk => self.jump_section(false, false),
            Command::NextFile => self.jump_section(true, true),
            Command::PreviousFile => self.jump_section(false, true),
            Command::Right if !self.wrap => self.move_horizontal(true)?,
            Command::Left if !self.wrap => self.move_horizontal(false)?,
            Command::SwitchSide => self.switch_side(),
            Command::ToggleLayout => {
                let layout = if self.split() {
                    Layout::Unified
                } else {
                    Layout::Split
                };
                self.reflow(layout, self.width, self.height)?;
            }
            Command::ToggleWrap => self.toggle_wrap()?,
            Command::ToggleNumbers => self.toggle_numbers()?,
            Command::ToggleTheme => self.toggle_theme()?,
            Command::Activate => highlight = self.expand_context()?,
            _ => return Ok(InputEffect::Ignored),
        }
        Ok(if highlight {
            InputEffect::Highlight
        } else {
            InputEffect::Redraw
        })
    }
}