aboutsummaryrefslogtreecommitdiffstats
path: root/sl0pstatus/src/bar_frame.rs
blob: 434024271b470d01de407000d60468b7726894db (plain) (blame)
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
155
156
157
158
159
160
161
162
163
use std::collections::BTreeMap;

use crate::render::FormattedPart;

/// sl0p fork: draw the bar as a framed box in the pane style, when it has the
/// rows to do it.
///
/// The bar is chrome sitting under a screenful of rounded, gapped panes, and a
/// bare line of text at the bottom edge does not read as part of that. Given
/// three rows it draws itself the same box the panes have; given one, it stays
/// a single line, which is what the responsive ("mobile") layout gives it on a
/// narrow terminal where two rows of decoration are two rows of content.
///
/// The height is the LAYOUT's decision, not this plugin's: a plugin cannot
/// resize its own pane, and the tab template that places the bar is the thing
/// that knows whether we are in the collapsed layout. Here we only adapt to the
/// rows we are given, which also means a `size=1` bar degrades to the old
/// single line rather than drawing a broken box.
#[derive(Debug)]
pub struct BarFrameConfig {
    pub enabled: bool,
    pub rounded: bool,
    pub format: FormattedPart,
}

impl Default for BarFrameConfig {
    fn default() -> Self {
        Self {
            // off unless asked for, so an unmodified zjstatus config renders
            // exactly as it did
            enabled: false,
            rounded: true,
            format: FormattedPart::default(),
        }
    }
}

/// The number of rows a framed bar needs: two borders and a line of content.
pub const FRAMED_ROWS: usize = 3;

pub struct FrameGlyphs {
    pub top_left: &'static str,
    pub top_right: &'static str,
    pub bottom_left: &'static str,
    pub bottom_right: &'static str,
    pub horizontal: &'static str,
    pub vertical: &'static str,
}

impl BarFrameConfig {
    pub fn glyphs(&self) -> FrameGlyphs {
        // matches zellij's own boundary_type constants, so the box is the same
        // one the panes draw and `rounded_corners` in config.kdl has a twin
        // here rather than a lookalike
        match self.rounded {
            true => FrameGlyphs {
                top_left: "╭",
                top_right: "╮",
                bottom_left: "╰",
                bottom_right: "╯",
                horizontal: "─",
                vertical: "│",
            },
            false => FrameGlyphs {
                top_left: "┌",
                top_right: "┐",
                bottom_left: "└",
                bottom_right: "┘",
                horizontal: "─",
                vertical: "│",
            },
        }
    }

    /// Wrap one rendered line of bar content in the box.
    ///
    /// `content` must already be exactly `inner_cols` wide; it is placed
    /// verbatim between the vertical borders, so whatever laid it out decided
    /// where every column of it lands.
    pub fn wrap(&self, content: &str, inner_cols: usize) -> String {
        let g = self.glyphs();
        let horizontal = g.horizontal.repeat(inner_cols);
        let top = self
            .format
            .format_string(&format!("{}{}{}", g.top_left, horizontal, g.top_right));
        let bottom = self.format.format_string(&format!(
            "{}{}{}",
            g.bottom_left, horizontal, g.bottom_right
        ));
        let left = self.format.format_string(g.vertical);
        let right = self.format.format_string(g.vertical);
        format!("{top}\n{left}{content}{right}\n{bottom}")
    }
}

pub fn parse_bar_frame_config(config: &BTreeMap<String, String>) -> BarFrameConfig {
    let default = BarFrameConfig::default();
    BarFrameConfig {
        enabled: config
            .get("bar_frame_enabled")
            .map(|value| value == "true")
            .unwrap_or(default.enabled),
        rounded: config
            .get("bar_frame_rounded")
            .map(|value| value == "true")
            .unwrap_or(default.rounded),
        format: match config.get("bar_frame_format") {
            Some(format) => FormattedPart::from_format_string(format, config),
            None => default.format,
        },
    }
}

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

    fn framed(rounded: bool) -> BarFrameConfig {
        BarFrameConfig {
            enabled: true,
            rounded,
            format: FormattedPart::default(),
        }
    }

    #[test]
    fn the_box_is_three_lines_of_equal_width() {
        let wrapped = framed(true).wrap("content--", 9);
        let lines: Vec<&str> = wrapped.split('\n').collect();
        assert_eq!(lines.len(), FRAMED_ROWS);
        for line in &lines {
            assert_eq!(
                console::measure_text_width(line),
                11,
                "line {line:?} is not inner_cols + 2 borders wide"
            );
        }
    }

    #[test]
    fn rounded_matches_the_pane_frame_glyphs() {
        let wrapped = framed(true).wrap(" ", 1);
        assert!(wrapped.starts_with('╭'), "{wrapped:?}");
        assert!(wrapped.ends_with('╯'), "{wrapped:?}");

        let square = framed(false).wrap(" ", 1);
        assert!(square.starts_with('┌'), "{square:?}");
        assert!(square.ends_with('┘'), "{square:?}");
    }

    #[test]
    fn the_content_is_placed_verbatim() {
        // the caller owns every column of the content line: this must not trim,
        // pad or re-align it, or the click math stops matching the drawing
        let wrapped = framed(true).wrap("  spaced  ", 10);
        assert!(wrapped.contains("│  spaced  │"), "{wrapped:?}");
    }

    #[test]
    fn disabled_by_default_for_an_unmodified_config() {
        assert!(!parse_bar_frame_config(&BTreeMap::new()).enabled);
    }
}