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) -> 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); } }