fundaia 0.55.3

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
pub fn with_colour(text: &str) -> String {
    strip(text, true)
}

pub fn plain(text: &str) -> String {
    strip(text, false)
}

fn strip(text: &str, keep_colour: bool) -> String {
    if !text
        .chars()
        .any(|c| c.is_control() && c != '\t' && c != '\n')
    {
        return text.to_owned();
    }

    let mut out = String::with_capacity(text.len());
    let mut chars = text.chars().peekable();

    while let Some(current) = chars.next() {
        match current {
            '\u{1b}' => match chars.next() {
                Some('[') => {
                    let sequence = read_csi(&mut chars);
                    if keep_colour && sequence.ends_with('m') && is_sgr(&sequence) {
                        out.push('\u{1b}');
                        out.push('[');
                        out.push_str(&sequence);
                    }
                }
                Some(']' | 'P' | 'X' | '^' | '_') => skip_string(&mut chars),
                Some(_) | None => {}
            },
            '\u{9b}' => {
                let sequence = read_csi(&mut chars);
                if keep_colour && sequence.ends_with('m') && is_sgr(&sequence) {
                    out.push('\u{1b}');
                    out.push('[');
                    out.push_str(&sequence);
                }
            }
            '\u{9d}' | '\u{90}' | '\u{98}' | '\u{9e}' | '\u{9f}' => skip_string(&mut chars),
            '\t' | '\n' => out.push(current),
            c if c.is_control() => {}
            c => out.push(c),
        }
    }

    out
}

fn read_csi(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
    let mut sequence = String::new();

    while let Some(&next) = chars.peek() {
        chars.next();
        sequence.push(next);
        if ('\u{40}'..='\u{7e}').contains(&next) {
            break;
        }
        if !('\u{20}'..='\u{3f}').contains(&next) {
            sequence.clear();
            break;
        }
    }

    sequence
}

fn is_sgr(sequence: &str) -> bool {
    sequence
        .strip_suffix('m')
        .is_some_and(|parameters| parameters.chars().all(|c| c.is_ascii_digit() || c == ';'))
}

fn skip_string(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
    while let Some(current) = chars.next() {
        match current {
            '\u{7}' | '\u{9c}' => return,
            '\u{1b}' => {
                if chars.peek() == Some(&'\\') {
                    chars.next();
                }
                return;
            }
            _ => {}
        }
    }
}

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

    #[test]
    fn it_should_keep_the_colour_of_a_log_line() {
        assert_eq!(
            with_colour("\u{1b}[1;32mok\u{1b}[0m"),
            "\u{1b}[1;32mok\u{1b}[0m"
        );
    }

    #[test]
    fn it_should_drop_an_osc_sequence_that_writes_the_clipboard() {
        assert_eq!(with_colour("a\u{1b}]52;c;Zm9v\u{7}b"), "ab");
    }

    #[test]
    fn it_should_drop_an_osc_sequence_ended_by_st() {
        assert_eq!(with_colour("a\u{1b}]0;title\u{1b}\\b"), "ab");
    }

    #[test]
    fn it_should_drop_cursor_movement_while_keeping_colour() {
        assert_eq!(
            with_colour("\u{1b}[2J\u{1b}[1A\u{1b}[31mred"),
            "\u{1b}[31mred"
        );
    }

    #[test]
    fn it_should_drop_the_eight_bit_csi() {
        assert_eq!(with_colour("x\u{9b}2Jy"), "xy");
    }

    #[test]
    fn it_should_drop_a_carriage_return_and_a_backspace() {
        assert_eq!(with_colour("ok\r\u{8}fail"), "okfail");
    }

    #[test]
    fn it_should_keep_tabs_and_newlines() {
        assert_eq!(with_colour("a\tb\nc"), "a\tb\nc");
    }

    #[test]
    fn it_should_strip_colour_from_a_name() {
        assert_eq!(plain("\u{1b}[31mapi\u{1b}[0m"), "api");
    }

    #[test]
    fn it_should_drop_an_unterminated_osc_to_the_end() {
        assert_eq!(plain("a\u{1b}]8;;http://x"), "a");
    }

    #[test]
    fn it_should_leave_ordinary_unicode_alone() {
        assert_eq!(plain("señal → ✔"), "señal → ✔");
    }
}