Skip to main content

cli_ui/prompt/
boxed.rs

1#![allow(missing_docs)]
2//! Bordered text block — ports `@clack/prompts` `box()`.
3//!
4//! Renders a configurable rounded or square box with a title and aligned body.
5
6use crate::styles::{paint, DIM, WHITE};
7
8#[derive(Clone, Copy)]
9pub enum Align {
10    Left,
11    Center,
12    Right,
13}
14
15pub struct BoxOptions {
16    pub title: String,
17    pub body: String,
18    pub title_align: Align,
19    pub content_align: Align,
20    pub title_padding: usize,
21    pub content_padding: usize,
22    pub width: Option<usize>,
23    pub rounded: bool,
24}
25
26pub fn boxed(body: impl Into<String>, title: impl Into<String>) -> BoxOptions {
27    BoxOptions {
28        title: title.into(),
29        body: body.into(),
30        title_align: Align::Left,
31        content_align: Align::Left,
32        title_padding: 1,
33        content_padding: 2,
34        width: None,
35        rounded: true,
36    }
37}
38
39impl BoxOptions {
40    pub fn title_align(mut self, a: Align) -> Self {
41        self.title_align = a;
42        self
43    }
44    pub fn content_align(mut self, a: Align) -> Self {
45        self.content_align = a;
46        self
47    }
48    pub fn title_padding(mut self, n: usize) -> Self {
49        self.title_padding = n;
50        self
51    }
52    pub fn content_padding(mut self, n: usize) -> Self {
53        self.content_padding = n;
54        self
55    }
56    pub fn width(mut self, n: usize) -> Self {
57        self.width = Some(n);
58        self
59    }
60    pub fn rounded(mut self, v: bool) -> Self {
61        self.rounded = v;
62        self
63    }
64
65    /// Print the box to stderr.
66    pub fn print(&self) {
67        eprintln!("{}", self.render());
68    }
69
70    pub fn render(&self) -> String {
71        let (tl, tr, bl, br) = if self.rounded {
72            ("╭", "╮", "╰", "╯")
73        } else {
74            ("┌", "┐", "└", "┘")
75        };
76        let h = "─";
77        let v = "│";
78
79        let body_lines: Vec<&str> = self.body.lines().collect();
80        let longest = body_lines
81            .iter()
82            .map(|l| l.chars().count())
83            .max()
84            .unwrap_or(0);
85        let title_len = self.title.chars().count();
86        let inner = self.width.unwrap_or_else(|| {
87            longest.max(title_len + self.title_padding * 2) + self.content_padding * 2
88        });
89
90        let mut out = String::new();
91        // top border with title
92        let (tl_pad, tr_pad) = pad_for(title_len, inner, self.title_padding, self.title_align);
93        out.push_str(&paint(DIM, tl));
94        out.push_str(&paint(DIM, &h.repeat(tl_pad)));
95        out.push_str(&paint(WHITE, &self.title));
96        out.push_str(&paint(DIM, &h.repeat(tr_pad)));
97        out.push_str(&paint(DIM, tr));
98        out.push('\n');
99
100        // body
101        for line in &body_lines {
102            let len = line.chars().count();
103            let (lp, rp) = pad_for(len, inner, self.content_padding, self.content_align);
104            out.push_str(&paint(DIM, v));
105            out.push_str(&" ".repeat(lp));
106            out.push_str(line);
107            out.push_str(&" ".repeat(rp));
108            out.push_str(&paint(DIM, v));
109            out.push('\n');
110        }
111        if body_lines.is_empty() {
112            out.push_str(&paint(DIM, v));
113            out.push_str(&" ".repeat(inner));
114            out.push_str(&paint(DIM, v));
115            out.push('\n');
116        }
117
118        // bottom
119        out.push_str(&paint(DIM, bl));
120        out.push_str(&paint(DIM, &h.repeat(inner)));
121        out.push_str(&paint(DIM, br));
122        out
123    }
124}
125
126fn pad_for(len: usize, inner: usize, default_pad: usize, align: Align) -> (usize, usize) {
127    let left = match align {
128        Align::Left => default_pad,
129        Align::Center => inner.saturating_sub(len) / 2,
130        Align::Right => inner.saturating_sub(len + default_pad),
131    };
132    let right = inner.saturating_sub(left + len);
133    (left, right)
134}