verco 6.3.1

A simple Git/Mercurial/PlasticSCM tui client based on keyboard shortcuts
use std::sync::Arc;

use crate::{application::EventSender, backend::Backend, platform::Key};

pub mod branches;
pub mod log;
pub mod revision_details;
pub mod status;
pub mod tags;

pub enum ModeResponse {
    Status(status::Response),
    Log(log::Response),
    RevisionDetails(revision_details::Response),
    Branches(branches::Response),
    Tags(tags::Response),
}

pub enum ModeKind {
    Status,
    Log,
    RevisionDetails(String),
    Branches,
    Tags,
}
impl Default for ModeKind {
    fn default() -> Self {
        Self::Status
    }
}

#[derive(Clone)]
pub struct ModeContext {
    pub backend: Arc<dyn Backend>,
    pub event_sender: EventSender,
    pub viewport_size: (u16, u16),
}

pub struct ModeStatus {
    pub pending_input: bool,
}

#[derive(Default)]
pub struct Output {
    text: String,
    line_count: usize,
    scroll: usize,
}
impl Output {
    pub fn set(&mut self, output: String) {
        self.text = output;
        self.line_count = self.text.lines().count();
        self.scroll = 0;
    }

    pub fn text(&self) -> &str {
        &self.text
    }

    pub fn line_count(&self) -> usize {
        self.line_count
    }

    pub fn lines_from_scroll<'a>(&'a self) -> impl 'a + Iterator<Item = &'a str> {
        self.text.lines().skip(self.scroll)
    }

    pub fn on_key(&mut self, available_height: usize, key: Key) {
        let half_height = available_height / 2;

        self.scroll = match key {
            Key::Down | Key::Ctrl('n') | Key::Char('j') => self.scroll + 1,
            Key::Up | Key::Ctrl('p') | Key::Char('k') => self.scroll.saturating_sub(1),
            Key::Ctrl('h') | Key::Home => 0,
            Key::Ctrl('e') | Key::End => usize::MAX,
            Key::Ctrl('d') | Key::PageDown => self.scroll + half_height,
            Key::Ctrl('u') | Key::PageUp => self.scroll.saturating_sub(half_height),
            _ => self.scroll,
        };

        self.scroll = self
            .line_count
            .saturating_sub(available_height)
            .min(self.scroll);
    }
}

#[derive(Default)]
pub struct ReadLine {
    input: String,
}
impl ReadLine {
    pub fn clear(&mut self) {
        self.input.clear();
    }

    pub fn input(&self) -> &str {
        &self.input
    }

    pub fn on_key(&mut self, key: Key) {
        match key {
            Key::Home | Key::Ctrl('u') => self.input.clear(),
            Key::Ctrl('w') => {
                fn is_word(c: char) -> bool {
                    c.is_alphanumeric() || c == '_'
                }

                fn rfind_boundary(mut chars: std::str::Chars, f: fn(&char) -> bool) -> usize {
                    match chars.rfind(f) {
                        Some(c) => chars.as_str().len() + c.len_utf8(),
                        None => 0,
                    }
                }

                let mut chars = self.input.chars();
                if let Some(c) = chars.next_back() {
                    let len = if is_word(c) {
                        rfind_boundary(chars, |&c| !is_word(c))
                    } else if c.is_ascii_whitespace() {
                        rfind_boundary(chars, |&c| is_word(c) || !c.is_ascii_whitespace())
                    } else {
                        rfind_boundary(chars, |&c| is_word(c) || c.is_ascii_whitespace())
                    };
                    self.input.truncate(len);
                }
            }
            Key::Backspace | Key::Ctrl('h') => {
                if let Some((last_char_index, _)) = self.input.char_indices().next_back() {
                    self.input.truncate(last_char_index);
                }
            }
            Key::Char(c) => self.input.push(c),
            _ => (),
        }
    }
}

pub enum SelectMenuAction {
    None,
    Toggle(usize),
    ToggleAll,
}

#[derive(Default)]
pub struct SelectMenu {
    pub cursor: usize,
    pub scroll: usize,
}
impl SelectMenu {
    pub fn saturate_cursor(&mut self, entries_len: usize) {
        self.cursor = entries_len.saturating_sub(1).min(self.cursor);
    }

    pub fn on_remove_entry(&mut self, index: usize) {
        if index <= self.cursor {
            self.cursor = self.cursor.saturating_sub(1);
        }
    }

    pub fn on_key(
        &mut self,
        entries_len: usize,
        available_height: usize,
        key: Key,
    ) -> SelectMenuAction {
        let half_height = available_height / 2;

        self.cursor = match key {
            Key::Down | Key::Ctrl('n') | Key::Char('j') => self.cursor + 1,
            Key::Up | Key::Ctrl('p') | Key::Char('k') => self.cursor.saturating_sub(1),
            Key::Ctrl('h') | Key::Home => 0,
            Key::Ctrl('e') | Key::End => usize::MAX,
            Key::Ctrl('d') | Key::PageDown => self.cursor + half_height,
            Key::Ctrl('u') | Key::PageUp => self.cursor.saturating_sub(half_height),
            _ => self.cursor,
        };

        self.saturate_cursor(entries_len);

        if self.cursor < self.scroll {
            self.scroll = self.cursor;
        } else if self.cursor >= self.scroll + available_height {
            self.scroll = self.cursor + 1 - available_height;
        }

        match key {
            Key::Char(' ') if self.cursor < entries_len => SelectMenuAction::Toggle(self.cursor),
            Key::Char('a') => SelectMenuAction::ToggleAll,
            _ => SelectMenuAction::None,
        }
    }
}

pub trait FilterEntry {
    fn fuzzy_matches(&self, pattern: &str) -> bool;
}

#[derive(Default)]
pub struct Filter {
    has_focus: bool,
    readline: ReadLine,
    visible_indices: Vec<usize>,
}
impl Filter {
    pub fn clear(&mut self) {
        self.has_focus = false;
        self.readline.clear();
        self.visible_indices.clear();
    }

    pub fn enter(&mut self) {
        self.has_focus = true;
        self.readline.clear();
    }

    pub fn on_key(&mut self, key: Key) {
        if key.is_submit() || key == Key::Ctrl('f') {
            self.has_focus = false;
        } else if key.is_cancel() {
            self.has_focus = false;
            self.readline.clear();
        } else {
            self.readline.on_key(key);
        }
    }

    pub fn filter<'entries, I, E>(&mut self, entries: I)
    where
        I: 'entries + Iterator<Item = &'entries E>,
        E: 'entries + FilterEntry,
    {
        self.visible_indices.clear();
        for (i, entry) in entries.enumerate() {
            if entry.fuzzy_matches(self.as_str()) {
                self.visible_indices.push(i);
            }
        }
    }

    pub fn on_remove_entry(&mut self, entry_index: usize) {
        for i in (0..self.visible_indices.len()).rev() {
            if entry_index < i {
                self.visible_indices[i] -= 1;
            } else if entry_index == i {
                self.visible_indices.remove(i);
            } else {
                break;
            }
        }
    }

    pub fn get_visible_index(&self, index: usize) -> Option<usize> {
        self.visible_indices.get(index).cloned()
    }

    pub fn visible_indices(&self) -> &[usize] {
        &self.visible_indices
    }

    pub fn is_filtering(&self) -> bool {
        self.has_focus || !self.readline.input().is_empty()
    }

    pub fn has_focus(&self) -> bool {
        self.has_focus
    }

    pub fn as_str(&self) -> &str {
        self.readline.input()
    }
}

pub fn fuzzy_matches(text: &str, pattern: &str) -> bool {
    let mut pattern_chars = pattern.chars();
    let mut pattern_char = match pattern_chars.next() {
        Some(c) => c,
        None => return true,
    };

    let mut previous_matched_index = 0;
    let mut was_alphanumeric = false;

    for (i, text_char) in text.char_indices() {
        if text_char.eq_ignore_ascii_case(&pattern_char) {
            let is_alphanumeric = text_char.is_ascii_alphanumeric();
            let matched = !is_alphanumeric || !was_alphanumeric || previous_matched_index + 1 == i;
            was_alphanumeric = is_alphanumeric;

            if matched {
                previous_matched_index = i;
                pattern_char = match pattern_chars.next() {
                    Some(c) => c,
                    None => return true,
                };
            }
        }
    }

    false
}