cursive-extras 0.13.4

Extra views for the Cursive TUI library as well some helper functions and macros
Documentation
use std::{
    fs,
    path::PathBuf,
    sync::LazyLock
};
use cursive_core::{
    View,
    Printer,
    Vec2,
    utils::{
        markup::StyledString,
        lines::spans::{LinesIterator, Row}
    },
    views::ScrollView,
    view::{ScrollStrategy, Scrollable},
    theme::{
        BaseColor,
        Color,
        Style,
        ColorStyle
    }
};
use rust_utils::logging::Log;
use regex::Regex;
use unicode_width::UnicodeWidthStr;
use crate::SpannedStrExt;

static INFO_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*INFO\]").unwrap());
static DBG_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*DEBUG\]").unwrap());
static WARN_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*WARN\]").unwrap());
static ERROR_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*ERROR\]").unwrap());
static FATAL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*FATAL\]").unwrap());

/// A special view that follows a specified log file
/// similar to how `tail -f <log file name>` tracks a log file
///
/// Auto-refresh in cursive must be set by calling `Cursive::set_fps()`
/// or `Cursive::set_autorefresh()` before using this view or it won't work
#[derive(Clone)]
pub struct LogView {
    path: PathBuf,
    content: LogContent
}

impl LogView {
    /// Create a new `LogView` with the specified log file path
    pub fn new<P: Into<PathBuf>>(path: P) -> LogView {
        let path = path.into();
        let raw_log = fs::read_to_string(&path).unwrap_or_default();
        let content = LogContent::new(raw_log);

        LogView {
            path,
            content
        }
    }

    /// Wrap the `LogView` in a `ScrollView` that jumps to the bottom when the log updates
    pub fn scroll_to_bottom(self) -> ScrollView<Self> {
        self.scrollable().scroll_strategy(ScrollStrategy::StickToBottom)
    }
}

impl View for LogView {
    fn draw(&self, printer: &Printer) {
        for y in 0..printer.size.y {
            printer.print_hline((0, y), printer.size.x, " ");
        }
        self.content.draw(printer);
    }

    fn required_size(&mut self, bound: Vec2) -> Vec2 {
        self.content.fit_to_width(bound.x);
        (bound.x, self.content.num_lines()).into()
    }

    fn layout(&mut self, size: Vec2) {
        let raw_log = fs::read_to_string(&self.path).unwrap_or_default();
        self.content.set_content(raw_log);
        self.content.fit_to_width(size.x);
    }
}

impl From<&Log> for LogView {
    /// Create a `LogView` from a `rust-utils` `Log` object
    ///
    /// This will follow the main log if it is enabled or the latest dated
    /// log file otherwise
    fn from(log: &Log) -> Self {
        let path = if let Some(main_log_path) = log.main_log_path() {
            main_log_path
        }
        else {
            log.log_path()
        };

        Self::new(path)
    }
}

#[derive(Clone)]
struct LogContent {
    content: StyledString,
    rows: Vec<Row>
}

impl LogContent {
    fn new(content: String) -> Self {
        let content = colorize_log(&content);
        LogContent {
            content,
            rows: Vec::new()
        }
    }

    // sets the content
    // since this might be expensive, the content
    // is only set when the new content is different from the current content
    fn set_content(&mut self, new_content: String) {
        if new_content.as_str() != self.content.source() {
            self.content = colorize_log(&new_content);
        }
    }

    // resize the log content to fit in the specified width
    fn fit_to_width(&mut self, width: usize) {
        if width == 0 { return; }
        self.rows = LinesIterator::new(self.content.as_spanned_str(), width).collect();
    }

    fn num_lines(&self) -> usize {
        self.rows.len()
    }

    fn draw(&self, printer: &Printer) {
        for (y, row) in self.rows.iter().enumerate() {
            let mut x = 0;
            for span in row.resolve(self.content.as_spanned_str()) {
                printer.with_style(*span.attr, |printer| {
                    printer.print((x, y), span.content);
                    x += span.content.width();
                });
            }
        }
    }
}

fn colorize_log(log: &str) -> StyledString {
    let mut styled_log = StyledString::new();

    let mut line_color = Style::from(Color::Light(BaseColor::White));
    for log_line in log.lines() {
        if INFO_RE.is_match(log_line) {
            line_color = Style::from(Color::Dark(BaseColor::Green));
        }
        else if DBG_RE.is_match(log_line) {
            line_color = Style::from(Color::Dark(BaseColor::Cyan));
        }
        else if WARN_RE.is_match(log_line) {
            line_color = Style::from(Color::Light(BaseColor::Yellow));
        }
        else if ERROR_RE.is_match(log_line) {
            line_color = Style::from(Color::Light(BaseColor::Red));
        }
        else if FATAL_RE.is_match(log_line) {
            line_color = Style::from(ColorStyle::new(Color::Light(BaseColor::Red), Color::Dark(BaseColor::Black)));
        }

        styled_log.append_styled(log_line, line_color);
        styled_log.append('\n');
    }

    styled_log
}