alacritty 0.11.0

A fast, cross-platform, OpenGL terminal emulator
use std::cmp::Ordering;
use std::iter::Skip;
use std::str::Chars;

use unicode_width::UnicodeWidthChar;

/// The action performed by [`StrShortener`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAction {
    /// Yield a spacer.
    Spacer,
    /// Terminate state reached.
    Terminate,
    /// Yield a shortener.
    Shortener,
    /// Yield a character.
    Char,
}

/// The direction which we should shorten.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ShortenDirection {
    /// Shorten to the start of the string.
    Left,

    /// Shorten to the end of the string.
    Right,
}

/// Iterator that yield shortened version of the text.
pub struct StrShortener<'a> {
    chars: Skip<Chars<'a>>,
    accumulted_len: usize,
    max_width: usize,
    direction: ShortenDirection,
    shortener: Option<char>,
    text_action: TextAction,
}

impl<'a> StrShortener<'a> {
    pub fn new(
        text: &'a str,
        max_width: usize,
        direction: ShortenDirection,
        mut shortener: Option<char>,
    ) -> Self {
        if text.is_empty() {
            // If we don't have any text don't produce a shortener for it.
            let _ = shortener.take();
        }

        if direction == ShortenDirection::Right {
            return Self {
                chars: text.chars().skip(0),
                accumulted_len: 0,
                text_action: TextAction::Char,
                max_width,
                direction,
                shortener,
            };
        }

        let mut offset = 0;
        let mut current_len = 0;

        let mut iter = text.chars().rev().enumerate();

        while let Some((idx, ch)) = iter.next() {
            let ch_width = ch.width().unwrap_or(1);
            current_len += ch_width;

            match current_len.cmp(&max_width) {
                // We can only be here if we've faced wide character or we've already
                // handled equality situation. Anyway, break.
                Ordering::Greater => break,
                Ordering::Equal => {
                    if shortener.is_some() && iter.clone().next().is_some() {
                        // We have one more character after, shortener will accumulate for
                        // the `current_len`.
                        break;
                    } else {
                        // The match is exact, consume shortener.
                        let _ = shortener.take();
                    }
                },
                Ordering::Less => (),
            }

            offset = idx + 1;
        }

        // Consume the iterator to count the number of characters in it.
        let num_chars = iter.last().map_or(offset, |(idx, _)| idx + 1);
        let skip_chars = num_chars - offset;

        let text_action = if current_len < max_width || shortener.is_none() {
            TextAction::Char
        } else {
            TextAction::Shortener
        };

        let chars = text.chars().skip(skip_chars);

        Self { chars, accumulted_len: 0, text_action, max_width, direction, shortener }
    }
}

impl<'a> Iterator for StrShortener<'a> {
    type Item = char;

    fn next(&mut self) -> Option<Self::Item> {
        match self.text_action {
            TextAction::Spacer => {
                self.text_action = TextAction::Char;
                Some(' ')
            },
            TextAction::Terminate => {
                // We've reached the termination state.
                None
            },
            TextAction::Shortener => {
                // When we shorten from the left we yield the shortener first and process the rest.
                self.text_action = if self.direction == ShortenDirection::Left {
                    TextAction::Char
                } else {
                    TextAction::Terminate
                };

                // Consume the shortener to avoid yielding it later when shortening left.
                self.shortener.take()
            },
            TextAction::Char => {
                let ch = self.chars.next()?;
                let ch_width = ch.width().unwrap_or(1);

                // Advance width.
                self.accumulted_len += ch_width;

                if self.accumulted_len > self.max_width {
                    self.text_action = TextAction::Terminate;
                    return self.shortener;
                } else if self.accumulted_len == self.max_width && self.shortener.is_some() {
                    // Check if we have a next char.
                    let has_next = self.chars.clone().next().is_some();

                    // We should terminate after that.
                    self.text_action = TextAction::Terminate;

                    return has_next.then(|| self.shortener.unwrap()).or(Some(ch));
                }

                // Add a spacer for wide character.
                if ch_width == 2 {
                    self.text_action = TextAction::Spacer;
                }

                Some(ch)
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn into_shortened_with_shortener() {
        let s = "Hello";
        let len = s.chars().count();
        assert_eq!(
            "",
            StrShortener::new("", 1, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        assert_eq!(
            ".",
            StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>()
        );

        assert_eq!(
            ".",
            StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        assert_eq!(
            "H.",
            StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>()
        );

        assert_eq!(
            ".o",
            StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        assert_eq!(
            s,
            &StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>()
        );

        assert_eq!(
            s,
            &StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        let s = "ちはP";
        let len = 2 + 2 + 1;
        assert_eq!(
            ".",
            &StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>()
        );

        assert_eq!(
            &".",
            &StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        assert_eq!(
            ".",
            &StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>()
        );

        assert_eq!(
            ".P",
            &StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        assert_eq!(
            "ち .",
            &StrShortener::new(s, 3, ShortenDirection::Right, Some('.')).collect::<String>()
        );

        assert_eq!(
            ".P",
            &StrShortener::new(s, 3, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        assert_eq!(
            "ち は P",
            &StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>()
        );

        assert_eq!(
            "ち は P",
            &StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>()
        );
    }

    #[test]
    fn into_shortened_without_shortener() {
        let s = "Hello";
        assert_eq!("", StrShortener::new("", 1, ShortenDirection::Left, None).collect::<String>());

        assert_eq!(
            "H",
            &StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>()
        );

        assert_eq!("o", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>());

        assert_eq!(
            "He",
            &StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>()
        );

        assert_eq!(
            "lo",
            &StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>()
        );

        assert_eq!(
            &s,
            &StrShortener::new(s, s.len(), ShortenDirection::Right, None).collect::<String>()
        );

        assert_eq!(
            &s,
            &StrShortener::new(s, s.len(), ShortenDirection::Left, None).collect::<String>()
        );

        let s = "こJんにちはP";
        let len = 2 + 1 + 2 + 2 + 2 + 2 + 1;
        assert_eq!("", &StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>());

        assert_eq!("P", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>());

        assert_eq!(
            "",
            &StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>()
        );

        assert_eq!("P", &StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>());

        assert_eq!(
            "こ J",
            &StrShortener::new(s, 3, ShortenDirection::Right, None).collect::<String>()
        );

        assert_eq!(
            "は P",
            &StrShortener::new(s, 3, ShortenDirection::Left, None).collect::<String>()
        );

        assert_eq!(
            "こ Jん に ち は P",
            &StrShortener::new(s, len, ShortenDirection::Left, None).collect::<String>()
        );

        assert_eq!(
            "こ Jん に ち は P",
            &StrShortener::new(s, len, ShortenDirection::Right, None).collect::<String>()
        );
    }
}