summaryrefslogtreecommitdiffstats
path: root/src/screen_text.rs
blob: 97d5d1336dcb0911302a549f685ac149889e40b1 (plain)
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
149
150
151
152
153
154
//! small things to help show some text on the terminal
//!
//! This isn't a generic library. Only features used in
//! broot are implemented.

use regex::Regex;
use std::io::{self, Write};
use termion::{color, style};

use crate::screens::{Screen, ScreenArea};

// write a string, taking exactly w chars, either
// by cutting or by padding
fn append_pad(dest: &mut String, s: &str, w: usize) {
    let s: Vec<char> = s.chars().collect();
    for i in 0..w {
        dest.push(if i < s.len() { s[i] } else { ' ' });
    }
}

// do some simple markdown-like formatting to ease text generation:
// - bold: **bold text**
// - code: `some code`
fn md(s: &str) -> String {
    lazy_static! {
        static ref bold_regex: Regex = Regex::new(r"\*\*([^*]+)\*\*").unwrap();
        static ref bold_repl: String = format!("{}$1{}", style::Bold, style::Reset);
        static ref code_regex: Regex = Regex::new(r"`([^`]+)`").unwrap();
        static ref code_repl: String = format!(
            "{} $1 {}",
            color::Bg(color::AnsiValue::grayscale(2)),
            color::Bg(color::Reset)
        );
    }
    let s = bold_regex.replace_all(s, &*bold_repl as &str); // TODO how to avoid this complex casting ?
    let s = code_regex.replace_all(&s, &*code_repl as &str);
    s.to_string()
}

/// A Text is a vec of lines
pub struct Text {
    lines: Vec<String>,
}
impl Text {
    pub fn new() -> Text {
        Text { lines: Vec::new() }
    }
    pub fn height(&self) -> usize {
        self.lines.len()
    }
    pub fn md(&mut self, line: &str) {
        self.lines.push(md(line));
    }
    pub fn push(&mut self, line: String) {
        self.lines.push(line);
    }
    // write the text in the area, taking into account the scrolled amount
    // and drawing a vertical scrollbar at the right if needed
    pub fn write(&self, screen: &mut Screen, area: &ScreenArea) -> io::Result<()> {
        let scrollbar = area.scrollbar();
        let mut i = area.scroll as usize;
        for y in area.top..=area.bottom {
            write!(
                screen.stdout,
                "{}{}",
                termion::cursor::Goto(1, y),
                termion::clear::CurrentLine,
            )?;
            if i < self.lines.len() {
                write!(screen.stdout, "{}", &self.lines[i],)?;
                i += 1;
            }
            if let Some((sctop, scbottom)) = scrollbar {
                if sctop <= y && y <= scbottom {
                    write!(screen.stdout, "{}▐", termion::cursor::Goto(screen.w, y),)?;
                }
            }
        }
        screen.stdout.flush()?;
        Ok(())
    }
}

/// A column in a TextTable
struct TextCol<'a, R> {
    title: String,
    extract: &'a Fn(&'a R) -> &str,
    width: usize, // padding and border not included
}

impl<'a, R> TextCol<'a, R> {}

/// A small utility to format some data in a tabular way on screen
pub struct TextTable<'a, R> {
    cols: Vec<TextCol<'a, R>>,
}

impl<'a, R> TextTable<'a, R> {
    pub fn new() -> TextTable<'a, R> {
        TextTable { cols: Vec::new() }
    }
    pub fn add_col(&mut self, title: &str, extract: &'a Fn(&'a R) -> &str) {
        let width = title.len(); // initial value, will change
        let title = title.to_string();
        self.cols.push(TextCol {
            title,
            extract,
            width,
        });
    }
    fn compute_col_widths(&mut self, rows: &'a [R]) {
        for row in rows {
            for c in &mut self.cols {
                c.width = c.width.max((c.extract)(row).len());
            }
        }
    }
    // write the table into the text.
    // Right now, to ease width computations, md transformation is done
    // only in the last column
    pub fn write(&mut self, rows: &'a [R], text: &mut Text) {
        lazy_static! {
            static ref bar: String = format!(
                " {}│{} ",
                color::Fg(color::AnsiValue::grayscale(8)),
                color::Fg(color::Reset),
            )
            .to_string();
        }
        self.compute_col_widths(&rows);
        let mut header = String::new();
        for col in &self.cols {
            header.push_str(&*bar);
            // we're lazy here:
            // we add some bold, and add 4 for the width because we know the * won't
            // show up on screen.
            append_pad(&mut header, &format!("**{}**", col.title), col.width + 4);
        }
        text.md(&header);
        for row in rows {
            let mut line = String::new();
            for (i, col) in self.cols.iter().enumerate() {
                line.push_str(&*bar);
                let s = (col.extract)(row);
                if i == self.cols.len() - 1 {
                    line.push_str(&md(s));
                } else {
                    append_pad(&mut line, s, col.width);
                }
            }
            text.push(line);
        }
    }
}