verco 6.5.4

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

use crate::mode::{Filter, Output, ReadLine, SelectMenu};

pub const HEADER_LINE_COUNT: usize = 2;
pub const RESERVED_LINES_COUNT: usize = HEADER_LINE_COUNT + 1;

pub static ENTER_ALTERNATE_BUFFER_CODE: &[u8] = b"\x1b[?1049h";
pub static EXIT_ALTERNATE_BUFFER_CODE: &[u8] = b"\x1b[?1049l";
pub static HIDE_CURSOR_CODE: &[u8] = b"\x1b[?25l";
pub static SHOW_CURSOR_CODE: &[u8] = b"\x1b[?25h";
pub static RESET_STYLE_CODE: &[u8] = b"\x1b[0;49m";
pub static BEGIN_TITLE_CODE: &[u8] = b"\x1b]0;";
pub static END_TITLE_CODE: &[u8] = b"\x07";

pub fn clear_until_new_line(buf: &mut Vec<u8>) {
    buf.extend_from_slice(b"\x1b[0K");
}

pub fn clear_to_end(buf: &mut Vec<u8>) {
    buf.extend_from_slice(b"\x1b[0J");
}

pub fn move_cursor_to_zero(buf: &mut Vec<u8>) {
    buf.extend_from_slice(b"\x1b[0;0H");
}

pub fn move_cursor_to_next_line(buf: &mut Vec<u8>) {
    buf.extend_from_slice(b"\x1b[1E");
}

pub fn set_background_color(buf: &mut Vec<u8>, color: Color) {
    buf.extend_from_slice(b"\x1b[48;5;");
    buf.extend_from_slice(color.code().as_bytes());
    buf.push(b'm');
}

static BEGIN_FOREGROUND_COLOR_CODE: &str = "\x1b[38;5;";
pub fn set_foreground_color(buf: &mut Vec<u8>, color: Color) {
    buf.extend_from_slice(BEGIN_FOREGROUND_COLOR_CODE.as_bytes());
    buf.extend_from_slice(color.code().as_bytes());
    buf.push(b'm');
}

#[derive(Clone, Copy)]
pub enum Color {
    Black,
    DarkRed,
    DarkGreen,
    DarkYellow,
    DarkBlue,
    DarkMagenta,
    DarkGray,
    White,
}
impl Color {
    fn code(&self) -> &str {
        match self {
            Self::Black => "0",
            Self::DarkRed => "1",
            Self::DarkGreen => "2",
            Self::DarkYellow => "3",
            Self::DarkBlue => "4",
            Self::DarkMagenta => "5",
            Self::DarkGray => "8",
            Self::White => "15",
        }
    }
}
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(BEGIN_FOREGROUND_COLOR_CODE)?;
        f.write_str(self.code())?;
        f.write_str("m")?;
        Ok(())
    }
}

pub trait SelectEntryDraw {
    fn draw(&self, drawer: &mut Drawer, hovered: bool, full: bool) -> usize;
}

pub struct Drawer {
    buf: Vec<u8>,
    pub viewport_size: (u16, u16),
}

impl Drawer {
    pub fn new(mut buf: Vec<u8>, viewport_size: (u16, u16)) -> Self {
        buf.clear();
        Self { buf, viewport_size }
    }

    pub fn take_buf(self) -> Vec<u8> {
        self.buf
    }

    pub fn clear_to_bottom(&mut self) {
        set_background_color(&mut self.buf, Color::Black);
        clear_to_end(&mut self.buf);
    }

    pub fn header(
        &mut self,
        current_mode_name: &str,
        left_help: &str,
        right_help: &str,
        spinner: u8,
    ) {
        const ALL_MODES: &[(&str, u8)] = &[
            ("status", b's'),
            ("log", b'l'),
            ("branches", b'b'),
            ("tags", b't'),
        ];
        fn mode_tabs_len(tabs: &[(&str, u8)]) -> usize {
            let mut len = 0;
            for (name, _) in tabs {
                len += "[x]".len() + name.len() + 1;
            }
            len
        }

        let background_color = Color::Black;
        let foreground_color = Color::DarkYellow;

        move_cursor_to_zero(&mut self.buf);

        set_background_color(&mut self.buf, background_color);
        set_foreground_color(&mut self.buf, foreground_color);
        self.buf.push(b' ');
        self.buf.push(spinner);
        self.buf.push(b' ');

        set_background_color(&mut self.buf, foreground_color);
        set_foreground_color(&mut self.buf, background_color);
        self.buf.push(b' ');
        self.buf.extend_from_slice(current_mode_name.as_bytes());
        self.buf.push(b' ');

        set_background_color(&mut self.buf, background_color);
        set_foreground_color(&mut self.buf, foreground_color);

        let (modes_before, modes_after) =
            match ALL_MODES.iter().position(|&(m, _)| m == current_mode_name) {
                Some(i) => (&ALL_MODES[..i], &ALL_MODES[i + 1..]),
                None => (ALL_MODES, &[][..]),
            };
        let modes_before_len = mode_tabs_len(modes_before);
        let modes_after_len = mode_tabs_len(modes_after);
        let current_mode_len = 3 + 1 + current_mode_name.len() + 1;

        let spacer_len = (self.viewport_size.0 as usize)
            .saturating_sub(modes_before_len + modes_after_len + current_mode_len);
        self.buf.extend(std::iter::repeat(b' ').take(spacer_len));

        for &(mode_name, shortcut) in modes_before.iter().chain(modes_after) {
            self.buf.push(b'[');
            self.buf.push(shortcut);
            self.buf.push(b']');
            self.buf.extend_from_slice(mode_name.as_bytes());
            self.buf.push(b' ');
        }

        clear_until_new_line(&mut self.buf);
        move_cursor_to_next_line(&mut self.buf);

        set_background_color(&mut self.buf, foreground_color);
        set_foreground_color(&mut self.buf, background_color);

        let mut left_help = left_help.as_bytes();
        let mut right_help = right_help.as_bytes();

        let available_width = self.viewport_size.0.saturating_sub(1) as usize;
        if left_help.len() > available_width {
            left_help = &left_help[..available_width];
            right_help = &[];
        } else if left_help.len() + right_help.len() > available_width {
            let overflow_len = left_help.len() + right_help.len() - available_width;
            right_help = &right_help[..right_help.len() - overflow_len];
        }

        let spacer_len = 1 + available_width - left_help.len() - right_help.len();
        self.buf.extend_from_slice(left_help);
        self.buf.extend(std::iter::repeat(b' ').take(spacer_len));
        self.buf.extend_from_slice(right_help);

        move_cursor_to_next_line(&mut self.buf);

        set_background_color(&mut self.buf, Color::Black);
        set_foreground_color(&mut self.buf, Color::White);
    }

    pub fn str(&mut self, line: &str) {
        self.buf.extend_from_slice(line.as_bytes());
    }

    pub fn fmt(&mut self, args: fmt::Arguments) {
        use std::io::Write;
        self.buf.write_fmt(args).unwrap();
    }

    pub fn next_line(&mut self) {
        clear_until_new_line(&mut self.buf);
        move_cursor_to_next_line(&mut self.buf);
    }

    pub fn diff(&mut self, output: &Output) -> usize {
        let tab_bytes = [b' '; 4];
        let mut utf8_buf = [0; 4];

        set_background_color(&mut self.buf, Color::Black);

        let mut line_count = 0;
        for line in output.lines_from_scroll() {
            let mut x = 0;

            match line.chars().next() {
                Some('+') => set_foreground_color(&mut self.buf, Color::DarkGreen),
                Some('-') => set_foreground_color(&mut self.buf, Color::DarkRed),
                _ => set_foreground_color(&mut self.buf, Color::White),
            }

            for c in line.chars() {
                match c {
                    '\t' => {
                        self.buf.extend_from_slice(&tab_bytes);
                        x += tab_bytes.len();
                    }
                    _ => {
                        let bytes = c.encode_utf8(&mut utf8_buf).as_bytes();
                        self.buf.extend_from_slice(bytes);
                        x += 1;
                    }
                }

                if x >= self.viewport_size.0 as _ {
                    x -= self.viewport_size.0 as usize;
                    line_count += 1;
                }
            }

            self.next_line();

            line_count += 1;
            if line_count + 1 >= self.viewport_size.1 as _ {
                break;
            }
        }

        set_foreground_color(&mut self.buf, Color::White);

        line_count
    }

    pub fn output(&mut self, output: &Output) -> usize {
        let tab_bytes = [b' '; 4];
        let mut utf8_buf = [0; 4];

        let mut line_count = 0;
        for line in output.lines_from_scroll() {
            let mut x = 0;
            for c in line.chars() {
                match c {
                    '\t' => {
                        self.buf.extend_from_slice(&tab_bytes);
                        x += tab_bytes.len();
                    }
                    _ => {
                        let bytes = c.encode_utf8(&mut utf8_buf).as_bytes();
                        self.buf.extend_from_slice(bytes);
                        x += 1;
                    }
                }

                if x >= self.viewport_size.0 as _ {
                    x -= self.viewport_size.0 as usize;
                    line_count += 1;
                }
            }

            self.next_line();

            line_count += 1;
            if line_count + 1 >= self.viewport_size.1 as _ {
                break;
            }
        }

        line_count
    }

    pub fn readline(&mut self, readline: &ReadLine, placeholder: &str) {
        let input = readline.input();

        set_background_color(&mut self.buf, Color::Black);
        set_foreground_color(&mut self.buf, Color::White);
        self.buf.extend_from_slice(input.as_bytes());

        set_background_color(&mut self.buf, Color::DarkRed);
        self.buf.push(b' ');
        set_background_color(&mut self.buf, Color::Black);

        if input.is_empty() {
            set_foreground_color(&mut self.buf, Color::DarkGray);
            self.buf.extend_from_slice(placeholder.as_bytes());
        }
    }

    pub fn filter(&mut self, filter: &Filter) -> usize {
        let text = filter.as_str();
        if !filter.is_filtering() {
            return 0;
        }

        const PREFIX: &str = "filter:";
        set_background_color(&mut self.buf, Color::DarkRed);
        set_foreground_color(&mut self.buf, Color::White);
        self.buf.extend_from_slice(PREFIX.as_bytes());

        let available_width = (self.viewport_size.0 as usize).saturating_sub(PREFIX.len() + 2);
        let (trimmed, text) = match text.char_indices().nth_back(available_width) {
            Some((i, _)) => (true, &text[i..]),
            None => (false, text),
        };
        self.buf.extend_from_slice(text.as_bytes());

        if filter.has_focus() {
            set_background_color(&mut self.buf, Color::White);
            self.buf.push(b' ');
            if !trimmed {
                set_background_color(&mut self.buf, Color::DarkRed);
            }
        }

        self.next_line();
        set_background_color(&mut self.buf, Color::Black);
        set_foreground_color(&mut self.buf, Color::White);

        1
    }

    pub fn select_menu<'entries, I, E>(
        &mut self,
        select: &SelectMenu,
        header_height: usize,
        show_full_hovered_entry: bool,
        entries: I,
    ) where
        I: 'entries + Iterator<Item = &'entries E>,
        E: 'entries + SelectEntryDraw,
    {
        let cursor_index = select.cursor;

        set_background_color(&mut self.buf, Color::Black);
        set_foreground_color(&mut self.buf, Color::White);

        let mut line_count = 0;
        let max_line_count =
            (self.viewport_size.1 as usize).saturating_sub(RESERVED_LINES_COUNT + header_height);

        for (i, entry) in entries.enumerate().skip(select.scroll) {
            let hovered = i == cursor_index;
            if hovered {
                set_background_color(&mut self.buf, Color::DarkMagenta);
            }

            line_count += entry.draw(self, hovered, hovered && show_full_hovered_entry);

            clear_until_new_line(&mut self.buf);
            move_cursor_to_next_line(&mut self.buf);

            if hovered {
                set_background_color(&mut self.buf, Color::Black);
            }

            if line_count >= max_line_count {
                break;
            }
        }
    }
}