lios-bar 0.1.1

A native GTK4/VTE sidebar workspace for navigating live Lios terminal sessions.
Documentation
const LARGE_PASTE_BYTES: usize = 4096;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Assessment {
    pub(crate) line_breaks: usize,
    pub(crate) lines: usize,
    pub(crate) characters: usize,
    pub(crate) bytes: usize,
    pub(crate) controls: usize,
    pub(crate) unusually_large: bool,
}

impl Assessment {
    pub(crate) fn requires_confirmation(self) -> bool {
        self.line_breaks > 0 || self.controls > 0 || self.unusually_large
    }

    pub(crate) fn detail(self) -> String {
        let mut reasons = Vec::new();
        if self.line_breaks > 0 {
            reasons.push(format!("{} line break(s)", self.line_breaks));
        }
        if self.controls > 0 {
            reasons.push(format!("{} unsafe control character(s)", self.controls));
        }
        if self.unusually_large {
            reasons.push(format!("{} UTF-8 bytes", self.bytes));
        }
        format!(
            "This paste contains {}. Total: {} line(s), {} character(s), {} byte(s). The clipboard contents are intentionally hidden.",
            reasons.join(", "),
            self.lines,
            self.characters,
            self.bytes
        )
    }
}

pub(crate) fn classify(text: &str) -> Assessment {
    let mut line_breaks = 0;
    let mut lines = usize::from(!text.is_empty());
    let mut controls = 0;
    let mut characters = 0;
    let mut previous_cr = false;

    for character in text.chars() {
        characters += 1;
        match character {
            '\r' => {
                line_breaks += 1;
                lines += 1;
                previous_cr = true;
            }
            '\n' => {
                if !previous_cr {
                    line_breaks += 1;
                    lines += 1;
                }
                previous_cr = false;
            }
            '\t' => previous_cr = false,
            character if character.is_control() => {
                controls += 1;
                previous_cr = false;
            }
            _ => previous_cr = false,
        }
    }

    Assessment {
        line_breaks,
        lines,
        characters,
        bytes: text.len(),
        controls,
        unusually_large: text.len() > LARGE_PASTE_BYTES,
    }
}

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

    #[test]
    fn single_line_and_tab_are_safe() {
        assert!(!classify("printf 'ok'\t").requires_confirmation());
    }

    #[test]
    fn newline_control_and_large_text_are_guarded() {
        assert!(classify("one\ntwo").requires_confirmation());
        assert!(classify("safe\u{1b}unsafe").requires_confirmation());
        assert!(classify(&"x".repeat(4097)).requires_confirmation());
    }

    #[test]
    fn detail_never_contains_clipboard_payload() {
        let secret = "secret-one\nsecret-two";
        let detail = classify(secret).detail();
        assert!(!detail.contains("secret-one"));
        assert!(detail.contains("line break"));
    }
}