rsmoji 0.2.0

Gitmoji, but oxidized!
use crossterm::cursor::{Hide, MoveDown, MoveToColumn, MoveUp, Show};
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, 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;

struct UIState<'a> {
    offset: usize,
    selection: usize,
    user_input: String,
    filtered_emojis: Vec<&'a str>,
}

fn main() -> io::Result<()> {
    let emojis: Vec<&'static str> = return_emojis();

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

    let mut state = UIState {
        offset: 0,
        selection: 2,
        user_input: String::new(),
        filtered_emojis: vec![""],
    };
    state.filtered_emojis = emojis
        .iter()
        .copied()
        .filter(|&emoji| {
            emoji
                .to_lowercase()
                .contains(&state.user_input.to_lowercase())
        })
        .collect();
    draw_menu(&state);

    enable_raw_mode().expect("Failed to enable raw mode");
    loop {
        let Event::Key(event) = read()? else {
            continue;
        };

        if event.kind != KeyEventKind::Press {
            continue;
        }

        let handle_keydown = |state: &mut UIState| {
            let offset_ = state.offset as isize;
            let selection_ = state.selection as isize;
            let max_selection_length_ = MAX_SELECTION_LENGTH as isize;

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

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

            redraw_menu(state);
        };

        let handle_char = |c: char, state: &mut UIState| {
            if c == 'c' && event.modifiers.contains(KeyModifiers::CONTROL) {
                cursor_to_start();
                disable_raw_mode().expect("Failed to disable raw mode");
                execute!(io::stdout(), Show).expect("Failed to unhide cursor");
                std::process::exit(0);
            }
            state.offset = 0;
            state.selection = 0;
            delete_menu(&state.filtered_emojis);
            state.user_input += &c.to_string();
            state.filtered_emojis = emojis
                .iter()
                .copied()
                .filter(|&emoji| {
                    emoji
                        .to_lowercase()
                        .contains(&state.user_input.to_lowercase())
                })
                .collect();
            draw_menu(state);
        };

        match event.code {
            KeyCode::Down => handle_keydown(&mut state),

            KeyCode::Up => {
                if state.offset > 0 {
                    state.offset -= 1;
                } else if state.selection >= 1 {
                    state.selection -= 1;
                }
                redraw_menu(&state);
            }

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

            KeyCode::Char(c) => handle_char(c, &mut state),

            KeyCode::Backspace => {
                delete_menu(&state.filtered_emojis);
                state.user_input.pop();
                state.filtered_emojis = emojis
                    .iter()
                    .copied()
                    .filter(|&emoji| {
                        emoji
                            .to_lowercase()
                            .contains(&state.user_input.to_lowercase())
                    })
                    .collect();
                draw_menu(&state);
            }
            _ => {}
        }
    }

    let gitmoji: Vec<&str> = state.filtered_emojis[state.offset + state.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 {
        let Event::Key(event) = read()? else {
            continue;
        };

        if event.kind != KeyEventKind::Press {
            continue;
        }

        let mut handle_char = |c: char| {
            if c == 'c' && event.modifiers.contains(KeyModifiers::CONTROL) {
                cursor_to_start();
                disable_raw_mode().expect("Failed to disable raw mode");
                execute!(io::stdout(), Show).expect("Failed to unhide cursor");
                std::process::exit(0);
            }
            commit_message += &c.to_string();
            reload_commit_message(&commit_message, false);
        };

        match event.code {
            KeyCode::Char(c) => handle_char(c),

            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(state: &UIState) {
    delete_menu(&state.filtered_emojis);
    draw_menu(state);
}

fn draw_menu(state: &UIState) {
    cursor_to_start();
    let user_input: String = state.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 == state.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 + state.offset < state.filtered_emojis.len() {
            execute!(
                io::stdout(),
                Print(state.filtered_emojis[i + state.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",
    ]
}