Skip to main content

rusty_rich/
box_drawing.rs

1//! Box drawing — equivalent to Rich's `box.py`.
2//!
3//! Defines various box styles (ROUNDED, SQUARE, HEAVY, etc.) using Unicode
4//! box-drawing characters, with ASCII-safe fallbacks.
5
6// ---------------------------------------------------------------------------
7// Box — defines characters for drawing a bordered box
8// ---------------------------------------------------------------------------
9
10/// A set of box-drawing characters defining the look of borders.
11///
12/// Layout of the 8-line string that defines a box:
13///
14/// ```text
15/// ┌─┬┐ top
16/// │ ││ head
17/// ├─┼┤ head_row
18/// │ ││ mid
19/// ├─┼┤ row
20/// ├─┼┤ foot_row
21/// │ ││ foot
22/// └─┴┘ bottom
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct BoxStyle {
26    // top row
27    pub top_left: char,
28    pub top: char,
29    pub top_divider: char,
30    pub top_right: char,
31    // head row (where content is on same line as top border)
32    pub head_left: char,
33    pub head_horizontal: char,
34    pub head_vertical: char,
35    pub head_right: char,
36    // head_row (separator after header)
37    pub head_row_left: char,
38    pub head_row_horizontal: char,
39    pub head_row_cross: char,
40    pub head_row_right: char,
41    // mid (between rows when show_lines is off)
42    pub mid_left: char,
43    pub mid_horizontal: char,
44    pub mid_vertical: char,
45    pub mid_right: char,
46    // row (between rows when show_lines is on)
47    pub row_left: char,
48    pub row_horizontal: char,
49    pub row_cross: char,
50    pub row_right: char,
51    // foot_row (separator before footer)
52    pub foot_row_left: char,
53    pub foot_row_horizontal: char,
54    pub foot_row_cross: char,
55    pub foot_row_right: char,
56    // foot
57    pub foot_left: char,
58    pub foot_horizontal: char,
59    pub foot_vertical: char,
60    pub foot_right: char,
61    // bottom row
62    pub bottom_left: char,
63    pub bottom: char,
64    pub bottom_divider: char,
65    pub bottom_right: char,
66    /// True if this box uses only ASCII characters.
67    pub ascii: bool,
68}
69
70impl BoxStyle {
71    /// Parse a box style from an 8-line string.
72    pub fn from_str(box_str: &str, ascii: bool) -> Self {
73        let lines: Vec<&str> = box_str.lines().collect();
74        assert_eq!(lines.len(), 8, "Box definition must have exactly 8 lines");
75
76        let line_chars: Vec<Vec<char>> = lines.iter()
77            .map(|l| l.chars().collect())
78            .collect();
79
80        // Each line should have 4 characters
81        for (i, chars) in line_chars.iter().enumerate() {
82            assert_eq!(chars.len(), 4, "Line {i} must have exactly 4 characters");
83        }
84
85        let l = &line_chars;
86        Self {
87            top_left: l[0][0], top: l[0][1], top_divider: l[0][2], top_right: l[0][3],
88            head_left: l[1][0], head_horizontal: l[1][1], head_vertical: l[1][2], head_right: l[1][3],
89            head_row_left: l[2][0], head_row_horizontal: l[2][1], head_row_cross: l[2][2], head_row_right: l[2][3],
90            mid_left: l[3][0], mid_horizontal: l[3][1], mid_vertical: l[3][2], mid_right: l[3][3],
91            row_left: l[4][0], row_horizontal: l[4][1], row_cross: l[4][2], row_right: l[4][3],
92            foot_row_left: l[5][0], foot_row_horizontal: l[5][1], foot_row_cross: l[5][2], foot_row_right: l[5][3],
93            foot_left: l[6][0], foot_horizontal: l[6][1], foot_vertical: l[6][2], foot_right: l[6][3],
94            bottom_left: l[7][0], bottom: l[7][1], bottom_divider: l[7][2], bottom_right: l[7][3],
95            ascii,
96        }
97    }
98
99    /// Get the plain text representation of the box definition.
100    pub fn to_string(&self) -> String {
101        format!(
102            "{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}\n{}{}{}{}",
103            self.top_left, self.top, self.top_divider, self.top_right,
104            self.head_left, self.head_horizontal, self.head_vertical, self.head_right,
105            self.head_row_left, self.head_row_horizontal, self.head_row_cross, self.head_row_right,
106            self.mid_left, self.mid_horizontal, self.mid_vertical, self.mid_right,
107            self.row_left, self.row_horizontal, self.row_cross, self.row_right,
108            self.foot_row_left, self.foot_row_horizontal, self.foot_row_cross, self.foot_row_right,
109            self.foot_left, self.foot_horizontal, self.foot_vertical, self.foot_right,
110            self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right,
111        )
112    }
113}
114
115impl std::fmt::Display for BoxStyle {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "{}", self.to_string())
118    }
119}
120
121// ---------------------------------------------------------------------------
122// Predefined box styles (matching Rich's defaults)
123// ---------------------------------------------------------------------------
124
125/// ASCII-only box.
126pub const ASCII: &str = "\
127+--+
128| ||
129|-+|
130| ||
131|-+|
132|-+|
133| ||
134+-++";
135
136/// ASCII with double edges.
137pub const ASCII2: &str = "\
138+-++
139| ||
140| ||
141| ||
142| ||
143| ||
144| ||
145+-++";
146
147/// Square box with double horizontal header separator.
148pub const SQUARE_DOUBLE_HEAD: &str = "\
149┌─┬┐
150│ ║│
151├─╪┤
152│ ││
153├─┼┤
154├─┼┤
155│ ││
156└─┴┘";
157
158/// Minimal box with double horizontal separator (head row only).
159pub const MINIMAL_DOUBLE_HEAD: &str = "  ═ \n  ═ \n  ═ \n    \n    \n    \n  ═ \n    ";
160
161/// Simple box with a single horizontal rule under the header.
162pub const SIMPLE_HEAD: &str = "    \n    \n    \n    \n    \n    \n ━┿ \n    ";
163
164/// ASCII box style with a double header line.
165pub const ASCII_DOUBLE_HEAD: &str = "\
166+-++
167| ||
168+=+|
169| ||
170|-+|
171|-+|
172| ||
173+-++";
174
175/// Rounded corners.
176pub const ROUNDED: &str = "\
177╭─┬╮
178│ ││
179├─┼┤
180│ ││
181├─┼┤
182├─┼┤
183│ ││
184╰─┴╯";
185
186/// Square corners.
187pub const SQUARE: &str = "\
188┌─┬┐
189│ ││
190├─┼┤
191│ ││
192├─┼┤
193├─┼┤
194│ ││
195└─┴┘";
196
197/// Heavy borders.
198pub const HEAVY: &str = "\
199┏━┳┓
200┃ ┃┃
201┣━╋┫
202┃ ┃┃
203┣━╋┫
204┣━╋┫
205┃ ┃┃
206┗━┻┛";
207
208/// Heavy edge, light inner.
209pub const HEAVY_EDGE: &str = "\
210┏━┯┓
211┃ │┃
212┠─┼┨
213┃ │┃
214┠─┼┨
215┠─┼┨
216┃ │┃
217┗━┷┛";
218
219/// Heavy header.
220pub const HEAVY_HEAD: &str = "\
221┏━┳┓
222┃ ┃┃
223┡━╇┩
224│ ││
225├─┼┤
226├─┼┤
227│ ││
228└─┴┘";
229
230/// Double borders.
231pub const DOUBLE: &str = "\
232╔═╦╗
233║ ║║
234╠═╬╣
235║ ║║
236╠═╬╣
237╠═╬╣
238║ ║║
239╚═╩╝";
240
241/// Double edge (like DOUBLE but inner is single).
242pub const DOUBLE_EDGE: &str = "\
243╔═╤╗
244║ │║
245╟─┼╢
246║ │║
247╟─┼╢
248╟─┼╢
249║ │║
250╚═╧╝";
251
252/// Simple (no borders, just vertical separators).
253pub const SIMPLE: &str = "    \n    \n ── \n    \n    \n ── \n    \n    ";
254
255/// Simple with heavy header.
256pub const SIMPLE_HEAVY: &str = "    \n    \n ━━ \n    \n    \n ━━ \n    \n    ";
257
258/// Minimal (just horizontal rule under header).
259pub const MINIMAL: &str = "\
260261262263
264
265
266
267  ╌ ";
268
269/// Minimal with heavy header.
270pub const MINIMAL_HEAVY: &str = "\
271272273274
275
276
277
278  ╍ ";
279
280// ---------------------------------------------------------------------------
281// Box style constants (lazily parsed)
282// ---------------------------------------------------------------------------
283
284use once_cell::sync::Lazy;
285
286/// Rounded box (default for Panel).
287pub static BOX_ROUNDED: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ROUNDED, false));
288/// Square-cornered box.
289pub static BOX_SQUARE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SQUARE, false));
290/// Heavy (thick) borders.
291pub static BOX_HEAVY: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(HEAVY, false));
292/// Heavy outer edges with light inner dividers.
293pub static BOX_HEAVY_EDGE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(HEAVY_EDGE, false));
294/// Heavy header row with regular body borders.
295pub static BOX_HEAVY_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(HEAVY_HEAD, false));
296/// Double-line borders.
297pub static BOX_DOUBLE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(DOUBLE, false));
298/// Double outer edge with single inner dividers.
299pub static BOX_DOUBLE_EDGE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(DOUBLE_EDGE, false));
300/// Simple borders (no vertical edges, horizontal rules only).
301pub static BOX_SIMPLE: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SIMPLE, false));
302/// Simple borders with heavy horizontal rules.
303pub static BOX_SIMPLE_HEAVY: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SIMPLE_HEAVY, false));
304/// Minimal box (just horizontal separators between header/body).
305pub static BOX_MINIMAL: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MINIMAL, false));
306/// Minimal box with heavy horizontal separators.
307pub static BOX_MINIMAL_HEAVY: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MINIMAL_HEAVY, false));
308/// ASCII-only box (uses `+`, `-`, `|` characters).
309pub static BOX_ASCII: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ASCII, true));
310/// ASCII box with doubled edges.
311pub static BOX_ASCII2: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ASCII2, true));
312/// Square box with a double horizontal header separator.
313pub static BOX_SQUARE_DOUBLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SQUARE_DOUBLE_HEAD, false));
314/// Minimal box with a double horizontal header separator.
315pub static BOX_MINIMAL_DOUBLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MINIMAL_DOUBLE_HEAD, false));
316/// Simple box with a single horizontal rule under the header.
317pub static BOX_SIMPLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(SIMPLE_HEAD, false));
318/// ASCII box with a double header line.
319pub static BOX_ASCII_DOUBLE_HEAD: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(ASCII_DOUBLE_HEAD, true));
320
321// ---------------------------------------------------------------------------
322// MARKDOWN box (no outer border)
323// ---------------------------------------------------------------------------
324
325/// Markdown-style box definition string (no outer borders).
326pub const MARKDOWN: &str = "    \n| ||\n|-||\n| ||\n|-||\n|-||\n| ||\n    ";
327
328/// Markdown-style box (no outer edges, vertical separators only).
329pub static BOX_MARKDOWN: Lazy<BoxStyle> = Lazy::new(|| BoxStyle::from_str(MARKDOWN, false));
330
331// ---------------------------------------------------------------------------
332// Safe box (for Windows legacy terminals)
333// ---------------------------------------------------------------------------
334
335/// Return an ASCII-safe version of a box if needed.
336pub fn get_safe_box(box_style: &BoxStyle, ascii_only: bool) -> BoxStyle {
337    if ascii_only && !box_style.ascii {
338        BOX_ASCII.clone()
339    } else {
340        box_style.clone()
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_rounded_box() {
350        let b = &*BOX_ROUNDED;
351        assert_eq!(b.top_left, '╭');
352        assert_eq!(b.bottom_right, '╯');
353    }
354
355    #[test]
356    fn test_box_from_str() {
357        let b = BoxStyle::from_str(ROUNDED, false);
358        assert_eq!(b, *BOX_ROUNDED);
359    }
360
361    #[test]
362    fn test_new_box_styles_parse() {
363        // Verify that the new box styles parse without panicking
364        let _ = &*BOX_SQUARE_DOUBLE_HEAD;
365        let _ = &*BOX_MINIMAL_DOUBLE_HEAD;
366        let _ = &*BOX_SIMPLE_HEAD;
367        let _ = &*BOX_ASCII_DOUBLE_HEAD;
368
369        // Spot-check characters
370        let sq = &*BOX_SQUARE_DOUBLE_HEAD;
371        assert_eq!(sq.top_left, '┌');
372        assert_eq!(sq.head_vertical, '║');
373        assert_eq!(sq.head_row_horizontal, '─');
374
375        let ac = &*BOX_ASCII_DOUBLE_HEAD;
376        assert_eq!(ac.head_row_left, '+');
377        assert_eq!(ac.head_row_horizontal, '=');
378    }
379}