1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
use crate::{
    messages::{Message, MessageLevel},
    render::tui::utils::{block_width, draw_text_with_ellipsis_nowrap, rect, sanitize_offset, VERTICAL_LINE},
    time::{format_time_for_messages, DATE_TIME_HMS},
};
use std::time::SystemTime;
use tui::{
    buffer::Buffer,
    layout::Rect,
    style::{Color, Modifier, Style},
    text::Span,
    widgets::{Block, Borders, Widget},
};
use unicode_width::UnicodeWidthStr;

pub fn pane(messages: &[Message], bound: Rect, overflow_bound: Rect, offset: &mut u16, buf: &mut Buffer) {
    let bold = Style::default().add_modifier(Modifier::BOLD);
    let block = Block::default()
        .title(Span::styled("Messages", bold))
        .borders(Borders::TOP);
    let inner_bound = block.inner(bound);
    block.render(bound, buf);
    let help_text = " ⨯ = `| ▢ = ~ ";
    draw_text_with_ellipsis_nowrap(rect::snap_to_right(bound, block_width(help_text)), buf, help_text, bold);

    let bound = inner_bound;
    *offset = sanitize_offset(*offset, messages.len(), bound.height);
    let max_origin_width = messages
        .iter()
        .rev()
        .skip(*offset as usize)
        .take(bound.height as usize)
        .fold(0, |state, message| state.max(block_width(&message.origin))) as u16;
    for (
        line,
        Message {
            time,
            message,
            level,
            origin,
        },
    ) in messages
        .iter()
        .rev()
        .skip(*offset as usize)
        .take(bound.height as usize)
        .enumerate()
    {
        let line_bound = rect::line_bound(bound, line);
        let (time_bound, level_bound, origin_bound, message_bound) = compute_bounds(line_bound, max_origin_width);
        if let Some(time_bound) = time_bound {
            draw_text_with_ellipsis_nowrap(time_bound, buf, format_time_column(time), None);
        }
        if let Some(level_bound) = level_bound {
            draw_text_with_ellipsis_nowrap(
                level_bound,
                buf,
                format_level_column(*level),
                Some(level_to_style(*level)),
            );
            draw_text_with_ellipsis_nowrap(rect::offset_x(level_bound, LEVEL_TEXT_WIDTH), buf, VERTICAL_LINE, None);
        }
        if let Some(origin_bound) = origin_bound {
            draw_text_with_ellipsis_nowrap(origin_bound, buf, origin, None);
            draw_text_with_ellipsis_nowrap(rect::offset_x(origin_bound, max_origin_width), buf, "→", None);
        }
        draw_text_with_ellipsis_nowrap(message_bound, buf, message, None);
    }

    if (bound.height as usize) < messages.len().saturating_sub(*offset as usize)
        || (*offset).min(messages.len() as u16) > 0
    {
        let messages_below = messages
            .len()
            .saturating_sub(bound.height.saturating_add(*offset) as usize);
        let messages_skipped = (*offset).min(messages.len() as u16);
        draw_text_with_ellipsis_nowrap(
            rect::offset_x(overflow_bound, 1),
            buf,
            format!("… {} skipped and {} more", messages_skipped, messages_below),
            bold,
        );
        let help_text = " ⇊ = D|↓ = J|⇈ = U|↑ = K ┘";
        draw_text_with_ellipsis_nowrap(
            rect::snap_to_right(overflow_bound, block_width(help_text)),
            buf,
            help_text,
            bold,
        );
    }
}

const LEVEL_TEXT_WIDTH: u16 = 4;
fn format_level_column(level: MessageLevel) -> &'static str {
    use MessageLevel::*;
    match level {
        Info => "info",
        Failure => "fail",
        Success => "done",
    }
}

fn level_to_style(level: MessageLevel) -> Style {
    use MessageLevel::*;
    Style::default()
        .fg(Color::Black)
        .add_modifier(Modifier::BOLD)
        .bg(match level {
            Info => Color::White,
            Failure => Color::Red,
            Success => Color::Green,
        })
}

fn format_time_column(time: &SystemTime) -> String {
    format!("{}{}", format_time_for_messages(*time), VERTICAL_LINE)
}

fn compute_bounds(line: Rect, max_origin_width: u16) -> (Option<Rect>, Option<Rect>, Option<Rect>, Rect) {
    let vertical_line_width = VERTICAL_LINE.width() as u16;
    let mythical_offset_we_should_not_need = 1;

    let time_bound = Rect {
        width: DATE_TIME_HMS as u16 + vertical_line_width,
        ..line
    };

    let mut cursor = time_bound.width + mythical_offset_we_should_not_need;
    let level_bound = Rect {
        x: cursor,
        width: LEVEL_TEXT_WIDTH + vertical_line_width,
        ..line
    };
    cursor += level_bound.width;

    let origin_bound = Rect {
        x: cursor,
        width: max_origin_width + vertical_line_width,
        ..line
    };
    cursor += origin_bound.width;

    let message_bound = rect::intersect(rect::offset_x(line, cursor), line);
    if message_bound.width < 30 {
        return (None, None, None, line);
    }
    (Some(time_bound), Some(level_bound), Some(origin_bound), message_bound)
}