rsmoji 0.1.2

Gitmoji, but oxidized!
use crossterm::cursor::{Hide, MoveDown, MoveToColumn, MoveUp, Show};
use crossterm::event::{Event, KeyCode, read};
use crossterm::execute;
use crossterm::style::{Attribute, Color::Rgb, Print, SetAttribute, SetForegroundColor};
use crossterm::terminal::{Clear, ClearType, disable_raw_mode, enable_raw_mode};
use std::io;
use std::process::Command;
use unicode_segmentation::UnicodeSegmentation;

const MAX_SELECTION_LENGTH: usize = 6;

fn main() -> io::Result<()> {
    let emojis: Vec<&'static str> = return_emojis();
    let mut selection: usize = 2;
    let mut offset: usize = 0;
    let mut user_input: String = String::new();

    execute!(io::stdout(), Hide).expect("Failed to hide cursor");

    let mut filtered_emojis: Vec<&str> = emojis
        .iter()
        .copied()
        .filter(|&emoji| emoji.to_lowercase().contains(&user_input.to_lowercase()))
        .collect();
    draw_menu(&filtered_emojis, offset, selection, &user_input);

    enable_raw_mode().expect("Failed to enable raw mode");
    loop {
        if let Event::Key(event) = read()? {
            match event.code {
                KeyCode::Down => {
                    let offset_ = offset as isize;
                    let selection_ = selection as isize;
                    let max_selection_length_ = MAX_SELECTION_LENGTH as isize;

                    if offset_ < filtered_emojis.len() as isize - max_selection_length_ {
                        offset += 1;
                    }

                    if selection >= return_length(&filtered_emojis) && !filtered_emojis.is_empty() {
                        selection = return_length(&filtered_emojis) - 1;
                    } else if selection_ < return_length(&filtered_emojis) as isize - 1
                        && offset_ >= filtered_emojis.len() as isize - max_selection_length_
                    {
                        selection += 1;
                    }

                    redraw_menu(&filtered_emojis, offset, selection, &user_input);
                }

                KeyCode::Up => {
                    if offset > 0 {
                        offset -= 1;
                    } else if selection >= 1 {
                        selection -= 1;
                    }
                    redraw_menu(&filtered_emojis, offset, selection, &user_input);
                }

                KeyCode::Enter => {
                    if !filtered_emojis.is_empty() {
                        delete_menu(&filtered_emojis);
                        break;
                    }
                }

                KeyCode::Char(c) => {
                    offset = 0;
                    selection = 0;
                    filtered_emojis = emojis
                        .iter()
                        .copied()
                        .filter(|&emoji| emoji.to_lowercase().contains(&user_input.to_lowercase()))
                        .collect();
                    delete_menu(&filtered_emojis);
                    user_input += &c.to_string();
                    filtered_emojis = emojis
                        .iter()
                        .copied()
                        .filter(|&emoji| emoji.to_lowercase().contains(&user_input.to_lowercase()))
                        .collect();
                    draw_menu(&filtered_emojis, offset, selection, &user_input);
                }
                KeyCode::Backspace => {
                    filtered_emojis = emojis
                        .iter()
                        .copied()
                        .filter(|&emoji| emoji.to_lowercase().contains(&user_input.to_lowercase()))
                        .collect();
                    delete_menu(&filtered_emojis);
                    user_input.pop();
                    filtered_emojis = emojis
                        .iter()
                        .copied()
                        .filter(|&emoji| emoji.to_lowercase().contains(&user_input.to_lowercase()))
                        .collect();
                    draw_menu(&filtered_emojis, offset, selection, &user_input);
                }
                _ => {}
            }
        }
    }

    let gitmoji: Vec<&str> = filtered_emojis[offset + selection]
        .graphemes(true)
        .collect();
    let gitmoji = gitmoji[0].to_string();
    let headline = "? Gitmoji: ".to_string() + &gitmoji + "!";
    cursor_to_start();
    execute!(
        io::stdout(),
        SetAttribute(Attribute::Bold),
        SetForegroundColor(Rgb {
            r: 180,
            g: 190,
            b: 254,
        }),
        Print(headline),
        SetAttribute(Attribute::Reset),
    )
    .expect("failed to print selected gitmoji");

    execute!(io::stdout(), MoveDown(2)).expect("Failed to move cursor down by two lines");
    let mut commit_message: String = String::new();
    reload_commit_message(&commit_message, false);
    loop {
        if let Event::Key(event) = read()? {
            match event.code {
                KeyCode::Char(c) => {
                    commit_message += &c.to_string();
                    reload_commit_message(&commit_message, false);
                }

                KeyCode::Backspace => {
                    commit_message.pop();
                    reload_commit_message(&commit_message, false);
                }

                KeyCode::Enter => {
                    reload_commit_message(&commit_message, true);
                    break;
                }
                _ => {}
            }
        }
    }

    let final_commit_message = gitmoji + " " + &commit_message;

    cursor_to_start();
    disable_raw_mode().expect("Failed to disable raw mode");
    execute!(io::stdout(), Show).expect("Failed to unhide cursor");
    Command::new("git")
        .args(["commit", "-m", final_commit_message.as_str()])
        .status()
        .expect("Failed to run git");

    Ok(())
}

fn reload_commit_message(commit_message: &String, end: bool) {
    let text = if end {
        "? Commit title: "
    } else {
        "? Enter commit title: "
    };
    let commit_message = commit_message.to_owned() + if end { "\n" } else { "โ–ˆ\n" };
    cursor_to_start();
    execute!(
        io::stdout(),
        MoveUp(1),
        Clear(ClearType::CurrentLine),
        SetAttribute(Attribute::Bold),
        SetForegroundColor(Rgb {
            r: 180,
            g: 190,
            b: 254,
        }),
        Print(text),
        SetAttribute(Attribute::Reset),
        Print(commit_message),
    )
    .expect("Failed to reload title input");
}

fn redraw_menu(emojis: &Vec<&str>, offset: usize, selection: usize, user_input: &String) {
    delete_menu(emojis);
    draw_menu(emojis, offset, selection, user_input);
}

fn draw_menu(emojis: &Vec<&str>, offset: usize, selection: usize, user_input: &String) {
    cursor_to_start();
    let user_input: String = user_input.to_string() + "โ–ˆ\n";
    execute!(
        io::stdout(),
        SetAttribute(Attribute::Bold),
        SetForegroundColor(Rgb {
            r: 180,
            g: 190,
            b: 254,
        }),
        Print("? Choose a gitmoji! ".to_string()),
        SetAttribute(Attribute::Reset),
        SetForegroundColor(Rgb {
            r: 186,
            g: 194,
            b: 222,
        }),
        Print(user_input),
        SetAttribute(Attribute::Reset),
    )
    .expect("Failed to print select text");
    for i in 0..MAX_SELECTION_LENGTH {
        cursor_to_start();
        if i == selection {
            execute!(
                io::stdout(),
                SetForegroundColor(Rgb {
                    r: 180,
                    g: 190,
                    b: 254,
                }),
                Print("โžœ ".to_string()),
            )
            .expect("Failed to print 'โžœ '")
        } else {
            execute!(io::stdout(), Print("  ".to_string())).expect("Failed to print '  '")
        }
        if i + offset < emojis.len() {
            execute!(
                io::stdout(),
                Print(emojis[i + offset]),
                SetAttribute(Attribute::Reset),
                Print("\n".to_string()),
            )
            .expect("Failed to print menu");
        }
    }
}

fn delete_menu(emojis: &Vec<&str>) {
    for _i in 0..return_length(emojis) + 1 {
        execute!(io::stdout(), MoveUp(1), Clear(ClearType::CurrentLine)).expect("Failed to clear");
    }
}

fn return_length(emojis: &Vec<&str>) -> usize {
    if emojis.len() > MAX_SELECTION_LENGTH {
        MAX_SELECTION_LENGTH
    } else {
        emojis.len()
    }
}

fn cursor_to_start() {
    execute!(io::stdout(), MoveToColumn(0))
        .expect("Failed to move cursor to the start of the line");
}

fn return_emojis() -> Vec<&'static str> {
    vec![
        "๐ŸŽจ - Improve structure / format of the code",
        "โšก๏ธ - Improve performance",
        "๐Ÿ”ฅ - Remove code or files",
        "๐Ÿ› - Fix a bug",
        "๐Ÿš‘๏ธ - Critical hotfix",
        "โœจ - Introduce new features",
        "๐Ÿ“ - Add or update documentation",
        "๐Ÿš€ - Deploy stuff",
        "๐Ÿ’„ - Add or update the UI and style files",
        "๐ŸŽ‰ - Begin a project",
        "โœ… - Add, update, or pass tests",
        "๐Ÿ”’๏ธ - Fix security or privacy issues",
        "๐Ÿ” - Add or update secrets",
        "๐Ÿ”– - Release / Version tags",
        "๐Ÿšจ - Fix compiler / linter warnings",
        "๐Ÿšง - Work in progress",
        "๐Ÿ’š - Fix CI Build",
        "โฌ‡๏ธ - Downgrade dependencies",
        "โฌ†๏ธ - Upgrade dependencies",
        "๐Ÿ“Œ - Pin dependencies to specific versions",
        "๐Ÿ‘ท - Add or update CI build system",
        "๐Ÿ“ˆ - Add or update analytics or track code",
        "โ™ป๏ธ - Refactor code",
        "โž• - Add a dependency",
        "โž– - Remove a dependency",
        "๐Ÿ”ง - Add or update configuration files",
        "๐Ÿ”จ - Add or update development scripts",
        "๐ŸŒ - Internationalization and localization",
        "โœ๏ธ - Fix typos",
        "๐Ÿ’ฉ - Write bad code that needs to be improved",
        "โช๏ธ - Revert changes",
        "๐Ÿ”€ - Merge branches",
        "๐Ÿ“ฆ๏ธ - Add or update compiled files or packages",
        "๐Ÿ‘ฝ๏ธ - Update code due to external API changes",
        "๐Ÿšš - Move or rename resources (e.g.: files, paths, routes)",
        "๐Ÿ“„ - Add or update license",
        "๐Ÿ’ฅ - Introduce breaking changes",
        "๐Ÿฑ - Add or update assets",
        "โ™ฟ๏ธ - Improve accessibility",
        "๐Ÿ’ก - Add or update comments in source code",
        "๐Ÿป - Write code drunkenly",
        "๐Ÿ’ฌ - Add or update text and literals",
        "๐Ÿ—ƒ๏ธ - Perform database related changes",
        "๐Ÿ”Š - Add or update logs",
        "๐Ÿ”‡ - Remove logs",
        "๐Ÿ‘ฅ - Add or update contributor(s)",
        "๐Ÿšธ - Improve user experience / usability",
        "๐Ÿ—๏ธ - Make architectural changes",
        "๐Ÿ“ฑ - Work on responsive design",
        "๐Ÿคก - Mock things",
        "๐Ÿฅš - Add or update an easter egg",
        "๐Ÿ™ˆ - Add or update a .gitignore file",
        "๐Ÿ“ธ - Add or update snapshots",
        "โš—๏ธ - Perform experiments",
        "๐Ÿ”๏ธ - Improve SEO",
        "๐Ÿท๏ธ - Add or update types",
        "๐ŸŒฑ - Add or update seed files",
        "๐Ÿšฉ - Add, update, or remove feature flags",
        "๐Ÿฅ… - Catch errors",
        "๐Ÿ’ซ - Add or update animations and transitions",
        "๐Ÿ—‘๏ธ - Deprecate code that needs to be cleaned up",
        "๐Ÿ›‚ - Work on code related to authorization, roles and permissions",
        "๐Ÿฉน - Simple fix for a non-critical issue",
        "๐Ÿง - Data exploration/inspection",
        "โšฐ๏ธ - Remove dead code",
        "๐Ÿงช - Add a failing test",
        "๐Ÿ‘” - Add or update business logic",
        "๐Ÿฉบ - Add or update healthcheck",
        "๐Ÿงฑ - Infrastructure related changes",
        "๐Ÿง‘โ€๐Ÿ’ป Improve developer experience",
        "๐Ÿ’ธ - Add sponsorships or money related infrastructure",
        "๐Ÿงต - Add or update code related to multithreading or concurrency",
        "๐Ÿฆบ - Add or update code related to validation",
        "โœˆ๏ธ - Improve offline support",
    ]
}