glint 6.3.4

a friendly tool for creating commits in the commitlint style
Documentation
use crate::color::reset_display;
use crate::git::{Git, GitStatus, GitStatusItem, GitStatusType};
use crate::Config;
use crate::TermBuffer;
use crossterm::{
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    style::{style, Color},
};
use std::iter;

#[derive(Debug)]
pub struct FilesPrompt<'a> {
    config: &'a Config,
    checked: Vec<bool>,
    focused_index: u16,
    options: GitStatus,
    git: &'a Git,
}

pub enum FilesPromptResult {
    Files(Vec<String>),
    Escape,
    Terminate,
}

impl<'a> FilesPrompt<'a> {
    pub fn new(config: &'a Config, git: &'a Git, options: GitStatus) -> Self {
        FilesPrompt {
            config,
            checked: (0..options.len()).map(|_| false).collect(),
            focused_index: 0,
            options,
            git,
        }
    }

    pub fn run(mut self) -> FilesPromptResult {
        let mut buffer = TermBuffer::new();

        let figlet = self
            .config
            .get_figlet()
            .expect("Ensure figlet_file points to a valid file, or remove it.");

        // Padded limit (never overflows by 1 item)
        let total = self.options.len();
        let max = 15;
        let take = if total > max { max - 3 } else { total };

        let mut first_iteration = true;
        loop {
            let mut event = if first_iteration {
                first_iteration = false;
                None
            } else {
                match event::read() {
                    Ok(Event::Key(KeyEvent { code, modifiers })) => Some((
                        code,
                        modifiers.contains(KeyModifiers::CONTROL),
                        modifiers.contains(KeyModifiers::SHIFT),
                        modifiers.contains(KeyModifiers::ALT),
                    )),
                    _ => continue,
                }
            };

            if let Some((ref mut key, _, _, _)) = event {
                // Vim-like navigation, since this prompt doesn't have text input
                // The right arrow strokes are also aliased to the diff shortcut, since
                // it's like going deeper into the tree
                *key = match key {
                    KeyCode::Char('q') => KeyCode::Esc,
                    KeyCode::Char('k') => KeyCode::Up,
                    KeyCode::Char('j') => KeyCode::Down,
                    KeyCode::Char('l') | KeyCode::Right => KeyCode::Char('d'),
                    _ => *key,
                };
            }

            match event {
                Some((KeyCode::Char('c'), true, false, false)) => {
                    return FilesPromptResult::Terminate;
                }
                Some((KeyCode::Char(' '), false, _, false)) => {
                    let index = self.focused_index as usize;
                    if index == 0 {
                        let set_to = !self.checked.iter().all(|&x| x);

                        for item in self.checked.iter_mut() {
                            *item = set_to;
                        }
                    } else {
                        self.checked[index - 1] = !self.checked[index - 1];
                    }
                }

                Some((KeyCode::Char('d'), _, _, _)) => {
                    let index = self.focused_index as usize;
                    if index == 0 {
                        let files: Vec<String> = vec![];
                        let _r = self.git.diff_less(files);
                    } else {
                        let option = self
                            .options
                            .iter()
                            .nth(index - 1)
                            .expect("diff should match a file");

                        if option.is_new() {
                            if option.is_dir() {
                                let _r = self
                                    .git
                                    .directory_untracked_less(option.file_name().as_ref());
                            } else {
                                let _r = self.git.less(option.file_name());
                            }
                        } else {
                            let files = vec![option.file_name().to_string()];
                            let _r = self.git.diff_less(files);
                        }
                    }
                }
                Some((KeyCode::Enter, _, _, _)) => {
                    let selected: Vec<String> = self
                        .options
                        .iter()
                        .enumerate()
                        .filter_map(|(i, file)| Some(file).filter(|_| self.checked[i]))
                        .map(Into::into)
                        .collect();
                    if !selected.is_empty() {
                        return FilesPromptResult::Files(selected);
                    }
                }

                Some((KeyCode::Esc, _, _, _)) => {
                    return FilesPromptResult::Escape;
                }
                Some((KeyCode::Up, _, _, true)) => {
                    self.focused_index = 0;
                }
                Some((KeyCode::Up, _, _, false)) => {
                    self.focused_index = match self.focused_index {
                        0 => 0,
                        x => x.saturating_sub(1),
                    };
                }
                Some((KeyCode::Down, _, _, true)) => {
                    self.focused_index += take as u16;
                }
                Some((KeyCode::Down, _, _, false)) => {
                    self.focused_index += 1;
                    if self.focused_index >= take as u16 + 1 {
                        self.focused_index = take as u16;
                    }
                }
                None => {}
                _ => continue,
            };

            let mut header = figlet.create_vec();
            figlet.write_to_buf_color("<glint>", header.as_mut_slice(), |s| {
                style(s).with(Color::Magenta).to_string()
            });

            for line in header {
                buffer.push_line(line);
            }

            let prompt_pre = "Toggle files to commit (with <space>, or tap 'd' for diff):";
            let underscores = "-".repeat(prompt_pre.len());
            buffer.push_line("");
            buffer.push_line(prompt_pre);
            buffer.push_line(format!("{}{}", underscores, reset_display()));

            let y_offset = buffer.lines() + self.focused_index;

            let focused_color = Color::Blue;
            let default_color = Color::Reset;

            let status_untracked = style('+').with(Color::Rgb {
                r: 96,
                g: 218,
                b: 177,
            });
            let status_modified = style('').with(Color::Rgb {
                r: 96,
                g: 112,
                b: 218,
            });
            let status_deleted = style('-').with(Color::Rgb {
                r: 218,
                g: 96,
                b: 118,
            });
            let status_none = style(' ');

            for (i, git_status_item) in iter::once(&GitStatusItem::new("<all>".to_owned()))
                .chain(self.options.iter().map(|item| item))
                .enumerate()
                .take(take + 1)
            {
                let line_color = if i as u16 == self.focused_index {
                    focused_color
                } else {
                    default_color
                };

                let checked = if i == 0 {
                    self.checked.iter().all(|&x| x)
                } else {
                    self.checked[i - 1]
                };
                let prefix = style(if checked { '' } else { '' }).with(line_color);

                let file_status = match *git_status_item.status() {
                    GitStatusType::Untracked => &status_untracked,
                    GitStatusType::Modified => &status_modified,
                    GitStatusType::Deleted => &status_deleted,
                    _ => &status_none,
                };

                let file_name = style(git_status_item.file_name()).with(line_color);

                let line = format!(
                    "{} {} {}{}",
                    prefix,
                    file_status,
                    file_name,
                    reset_display(),
                );
                buffer.push_line(line);
            }

            if take < total {
                let diff = total - take;
                buffer.push_line(format!("and {} more", diff));
            }

            buffer.set_next_cursor((0, y_offset));
            buffer.render_frame();
            buffer.flush();
        }
    }
}