verco 5.5.1

A simple Git/Hg version control client based on keyboard shortcuts
use std::io::Write;

use crossterm::{
    cursor, handle_command, queue,
    style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
    terminal::{self, Clear, ClearType},
    Result,
};

pub const SELECTED_BG_COLOR: Color = Color::Rgb {
    r: 80,
    g: 80,
    b: 80,
};
pub const ENTRY_COLOR: Color = Color::Rgb {
    r: 255,
    g: 180,
    b: 100,
};

pub const LOG_COLORS: &[Color] = &[
    Color::White,
    Color::Rgb {
        r: 211,
        g: 153,
        b: 33,
    },
    Color::Rgb {
        r: 52,
        g: 113,
        b: 134,
    },
    Color::Rgb {
        r: 137,
        g: 151,
        b: 29,
    },
    Color::Rgb {
        r: 251,
        g: 73,
        b: 47,
    },
    Color::White,
];

const HEADER_COLOR: Color = Color::Black;
const ACTION_COLOR: Color = Color::White;
const HEADER_BG_WAITING_COLOR: Color = Color::Magenta;
const HEADER_BG_WAITING_DARK_COLOR: Color = Color::DarkMagenta;
const HEADER_BG_OK_COLOR: Color = Color::Green;
const HEADER_BG_OK_DARK_COLOR: Color = Color::DarkGreen;
const HEADER_BG_ERROR_COLOR: Color = Color::Red;
const HEADER_BG_ERROR_DARK_COLOR: Color = Color::DarkRed;

const FILTER_COLOR: Color = Color::Black;
const FILTER_ACTIVE_BG_COLOR: Color = Color::Rgb {
    r: 255,
    g: 180,
    b: 100,
};
const FILTER_INACTIVE_BG_COLOR: Color = Color::Rgb {
    r: 180,
    g: 90,
    b: 50,
};

const HEADER_PREFIX: &str = concat!(env!("CARGO_PKG_NAME"), " @ ");
const DIR_NAME_MAX_LENGTH: usize = 32;

pub enum HeaderKind {
    Waiting,
    Ok,
    Error,
}

pub struct Header<'a> {
    pub action_name: &'a str,
    pub directory_name: &'a str,
}

impl<'a> Header<'a> {
    pub fn full_length(&self) -> usize {
        HEADER_PREFIX.len()
            + self.directory_name.len()
            + 3
            + self.action_name.len()
    }

    pub fn min_length(&self) -> usize {
        HEADER_PREFIX.len()
            + self.directory_name.len().min(DIR_NAME_MAX_LENGTH)
            + 3
            + self.action_name.len()
    }
}

pub fn show_header<W>(
    write: &mut W,
    header: Header,
    kind: HeaderKind,
    terminal_size: TerminalSize,
) -> Result<()>
where
    W: Write,
{
    let background_color = match kind {
        HeaderKind::Waiting => HEADER_BG_WAITING_COLOR,
        HeaderKind::Ok => HEADER_BG_OK_COLOR,
        HeaderKind::Error => HEADER_BG_ERROR_COLOR,
    };

    let background_dark_color = match kind {
        HeaderKind::Waiting => HEADER_BG_WAITING_DARK_COLOR,
        HeaderKind::Ok => HEADER_BG_OK_DARK_COLOR,
        HeaderKind::Error => HEADER_BG_ERROR_DARK_COLOR,
    };

    let status = match kind {
        HeaderKind::Waiting => "waiting",
        HeaderKind::Ok => "ok",
        HeaderKind::Error => "error",
    };

    let header_prefix;
    let directory_name;

    let terminal_width = terminal_size.width as usize;
    let mut padding = 0;

    fn fit(
        terminal_width: usize,
        header_length: usize,
        status: &str,
        padding: &mut usize,
    ) -> bool {
        let needed_width = header_length + status.len() + 2;
        if terminal_width >= needed_width {
            *padding = terminal_width - needed_width;
            true
        } else {
            false
        }
    }

    if fit(terminal_width, header.full_length(), status, &mut padding) {
        header_prefix = HEADER_PREFIX;
        directory_name = header.directory_name;
    } else if fit(terminal_width, header.min_length(), status, &mut padding) {
        header_prefix = HEADER_PREFIX;
        directory_name = &header.directory_name
            [(header.directory_name.len() - DIR_NAME_MAX_LENGTH)..];
    } else {
        panic!("window too small");
    }

    queue!(
        write,
        Clear(ClearType::All),
        cursor::MoveTo(0, 0),
        SetBackgroundColor(background_color),
        SetForegroundColor(HEADER_COLOR),
        Print(header_prefix),
        Print(directory_name),
        Print(' '),
        SetBackgroundColor(background_dark_color),
        SetForegroundColor(ACTION_COLOR),
        Print(' '),
        Print(header.action_name),
        Print(' '),
        SetBackgroundColor(background_color),
        SetForegroundColor(HEADER_COLOR),
        Print(" ".repeat(padding)),
        SetBackgroundColor(background_dark_color),
        SetForegroundColor(ACTION_COLOR),
        Print(' '),
        Print(status),
        Print(' '),
        ResetColor,
        cursor::MoveToNextLine(1),
    )
}

#[derive(Default, Clone, Copy)]
pub struct TerminalSize {
    pub width: u16,
    pub height: u16,
}

impl TerminalSize {
    pub fn get() -> Result<Self> {
        let size = terminal::size()?;
        Ok(Self {
            width: size.0,
            height: size.1,
        })
    }
}

#[derive(Clone, Copy)]
pub struct AvailableSize {
    pub width: usize,
    pub height: usize,
}

impl AvailableSize {
    pub fn from_temrinal_size(terminal_size: TerminalSize) -> Self {
        Self {
            width: terminal_size.width as usize,
            height: terminal_size.height as usize - 2,
        }
    }
}

pub fn move_cursor(
    scroll: &mut usize,
    cursor: &mut usize,
    available_size: AvailableSize,
    entry_count: usize,
    delta: i32,
) {
    if entry_count == 0 {
        *scroll = 0;
        *cursor = 0;
        return;
    }

    let previous_cursor = *cursor;
    let target_cursor = *cursor as i32 + delta;
    *cursor = if target_cursor < 0 {
        if previous_cursor == 0 {
            (target_cursor + entry_count as i32) as usize % entry_count
        } else {
            0
        }
    } else if target_cursor >= entry_count as i32 {
        if previous_cursor == entry_count - 1 {
            (target_cursor + entry_count as i32) as usize % entry_count
        } else {
            entry_count - 1
        }
    } else {
        target_cursor as usize
    };

    if cursor < scroll {
        *scroll = *cursor;
    } else if *cursor >= *scroll + available_size.height - 1 {
        *scroll = 1 + *cursor - available_size.height;
    }
}

pub fn fuzzy_matches(text: &str, pattern: &[char]) -> bool {
    let pattern_len = pattern.len();
    let mut pattern_index = 0;
    for c in text.chars() {
        if pattern_index >= pattern_len {
            break;
        }

        if pattern[pattern_index] == c {
            pattern_index += 1;
        }
    }

    pattern_index >= pattern_len
}

pub fn draw_filter_bar<W>(
    write: &mut W,
    filter: &[char],
    active: bool,
) -> Result<()>
where
    W: Write,
{
    if !active && filter.len() == 0 {
        return Ok(());
    }

    let bg_color = if active {
        FILTER_ACTIVE_BG_COLOR
    } else {
        FILTER_INACTIVE_BG_COLOR
    };

    queue!(
        write,
        cursor::MoveTo(0, 9999),
        SetBackgroundColor(bg_color),
        SetForegroundColor(FILTER_COLOR),
        Print('/'),
    )?;

    for c in filter {
        handle_command!(write, Print(c))?;
    }

    queue!(write, Clear(ClearType::UntilNewLine), ResetColor)?;
    Ok(())
}