Skip to main content

boxy_cli/
boxer.rs

1//! The main crate logic
2
3use crate::constructs::SegColor;
4use crate::constructs::*;
5use crate::templates::*;
6use colored::{Color, Colorize};
7use std::borrow::Cow;
8use std::fmt::Write;
9
10/// The main struct that represents a text box for CLI display.
11///
12/// `Boxy` contains all the configuration and content needed to render a styled text box
13/// in the terminal, including borders, text content, colors, padding, and alignment options.
14///
15/// # Examples
16///
17/// ```
18/// use boxy_cli::prelude::*;
19///
20/// // Create a simple text box
21/// let mut my_box = Boxy::new(BoxType::Double, "#00ffff");
22/// my_box.add_text_sgmt("Hello, World!", "#ffffff", BoxAlign::Center);
23/// my_box.display();
24/// ```
25#[derive(Debug)]
26pub struct Boxy<'a> {
27    type_enum: BoxType,
28    data: Vec<SegType<'a>>,
29    sect_count: usize,
30    box_col: Color,
31    colors: Vec<SegColor>,
32    int_padding: BoxPad,
33    ext_padding: BoxPad,
34    align: BoxAlign,
35    seg_align: Vec<BoxAlign>,
36    fixed_width: usize,
37    fixed_height: usize,
38    seg_cols_count: Vec<usize>,
39    seg_cols_ratio: Vec<Vec<usize>>,
40    terminal_width_offset: i32,
41}
42
43// Default struct values for the textbox
44impl Default for Boxy<'_> {
45    fn default() -> Self {
46        Self {
47            type_enum: BoxType::Single,
48            data: Vec::<SegType>::new(),
49            sect_count: 0usize,
50            box_col: SegColor::parse_hexcolor("#ffffff"),
51            colors: Vec::<SegColor>::new(),
52            int_padding: BoxPad::new(),
53            ext_padding: BoxPad::new(),
54            align: BoxAlign::Left,
55            seg_align: Vec::<BoxAlign>::new(),
56            fixed_width: 0usize,
57            fixed_height: 0usize,
58            seg_cols_ratio: Vec::<Vec<usize>>::new(),
59            seg_cols_count: Vec::<usize>::new(),
60            terminal_width_offset: -20,
61        }
62    }
63}
64
65const DEFAULT_PAD: BoxPad = BoxPad {
66    top: 1,
67    left: 1,
68    down: 1,
69    right: 1,
70};
71
72impl<'a> Boxy<'a> {
73    /// Creates a new instance of the `Boxy` struct with the specified border type and color.
74    ///
75    /// # Arguments
76    ///
77    /// * `box_type` - The border style to use from the `BoxType` enum
78    /// * `box_color` - Hex color code (e.g. `\"#ffffff\"`) for the border. Falls back to white with a stderr warning on invalid input
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use boxy_cli::prelude::*;
84    ///
85    /// let mut my_box = Boxy::new(BoxType::Double, "#00ffff");
86    /// ```
87    pub fn new(box_type: BoxType, box_color: &str) -> Self {
88        Boxy {
89            type_enum: box_type,
90            box_col: SegColor::parse_hexcolor(box_color),
91            ..Self::default()
92        }
93    }
94    /// Returns a new `BoxyBuilder` to create a text box using the builder pattern.
95    ///
96    /// The builder pattern provides a more fluent interface for configuring and creating a `Boxy` instance.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use boxy_cli::prelude::*;
102    ///
103    /// let my_box = Boxy::builder()
104    ///     .box_type(BoxType::Double)
105    ///     .color("#00ffff")
106    ///     .add_segment("Hello, World!", "#ffffff", BoxAlign::Center)
107    ///     .build();
108    /// ```
109    pub fn builder() -> BoxyBuilder<'a> {
110        BoxyBuilder::new()
111    }
112
113    /// Adds a new plain-text segment to the box, separated from previous segments by a
114    /// horizontal divider.
115    ///
116    /// Each call creates one distinct segment. Text is automatically word-wrapped to fit
117    /// the available width. For additional lines within the same segment (no divider between
118    /// them), use [`add_text_line`](Self::add_text_line) after this call.
119    ///
120    /// # Arguments
121    ///
122    /// * `data_string` - The text content for this segment
123    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
124    /// * `text_align` - How text is aligned within this segment: left, center, or right
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use boxy_cli::prelude::*;
130    ///
131    /// let mut b = Boxy::new(BoxType::Single, "#00ffff");
132    /// b.add_text_sgmt("Header", "#ffffff", BoxAlign::Center);
133    /// b.add_text_sgmt("Body text below a divider", "#aaaaaa", BoxAlign::Left);
134    /// b.display();
135    /// ```
136    pub fn add_text_sgmt(&mut self, data_string: &str, color: &str, text_align: BoxAlign) {
137        self.data
138            .push(SegType::Single(vec![Cow::from(data_string.to_owned())]));
139        self.colors
140            .push(SegColor::Single(vec![SegColor::parse_hexcolor(color)]));
141        self.seg_align.push(text_align);
142        self.sect_count += 1;
143        self.seg_cols_count.push(0);
144        self.seg_cols_ratio.push(vec![1]);
145    }
146
147    /// Adds a new columnar segment to the text box, separated by a horizontal divider.
148    ///
149    /// This sets up an empty segment with `column_count` side-by-side columns. Unlike
150    /// [`add_text_sgmt`](Self::add_text_sgmt), it doesn't take any text content directly —
151    /// columns start out empty and are populated afterwards with
152    /// [`add_col_text_line`](Self::add_col_text_line) or
153    /// [`add_col_text_line_indx`](Self::add_col_text_line_indx). By default, all columns are
154    /// given equal width; use [`set_segment_ratios`](Self::set_segment_ratios) to customize
155    /// the width ratio between columns.
156    ///
157    /// # Arguments
158    ///
159    /// * `text_align` - The alignment (left, center, right) applied to text within each column
160    /// * `column_count` - The number of columns in this segment (must be at least 1)
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use boxy_cli::prelude::*;
166    ///
167    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
168    /// my_box.add_col_text_sgmt(BoxAlign::Left, 2);
169    /// my_box.add_col_text_line("Left column text", "#ffffff", &0usize);
170    /// my_box.add_col_text_line("Right column text", "#ffffff", &1usize);
171    /// ```
172    ///
173    /// # Panics
174    ///
175    /// Panics if `column_count` is 0.
176    pub fn add_col_text_sgmt(&mut self, text_align: BoxAlign, column_count: usize) {
177        assert!(
178            column_count > 0,
179            "add_col_text_sgmt: column_count must be at least 1"
180        );
181        self.data
182            .push(SegType::Columnar(vec![Vec::new(); column_count]));
183        //colors are shaped to mirror data: one color-per-line, per columns
184        self.colors
185            .push(SegColor::Columnar(vec![Vec::new(); column_count]));
186        self.seg_align.push(text_align);
187        self.sect_count += 1;
188        self.seg_cols_count.push(column_count);
189        self.seg_cols_ratio.push(vec![1; column_count]); // default to equal width
190    }
191
192    /// Adds a new text line to the segment with a specific index.
193    ///
194    /// This method allows adding additional lines of text to an existing segment by specifying
195    /// the segment's index. The new line will appear below the existing content in that segment.
196    ///
197    /// # Arguments
198    ///
199    /// * `data_string` - The text content to add
200    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
201    /// * `seg_index` - The index of the segment to add this line to (0-based)
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use boxy_cli::prelude::*;
207    ///
208    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
209    /// my_box.add_text_sgmt("First segment", "#ffffff", BoxAlign::Left);
210    /// my_box.add_text_sgmt("Second segment", "#ffffff", BoxAlign::Left);
211    ///
212    /// // Add a line to the first segment (index 0)
213    /// my_box.add_text_line_indx("Additional line in first segment", "#32CD32", 0);
214    /// ```
215    ///
216    /// # Panics
217    ///
218    /// Panics if `seg_index` is out of bounds, or if the segment at that index is a
219    /// columnar segment — use [`add_col_text_line_indx`](Self::add_col_text_line_indx) for those.
220    pub fn add_text_line_indx(&mut self, data_string: &str, color: &str, seg_index: usize) {
221        match &mut self.data[seg_index] {
222            SegType::Single(lines) => lines.push(Cow::from(data_string.to_owned())),
223            SegType::Columnar(_) => panic!("add_text_line_indx called on Columnar segment!"),
224        }
225        match &mut self.colors[seg_index] {
226            SegColor::Single(cols) => cols.push(SegColor::parse_hexcolor(color)),
227            SegColor::Columnar(_) => panic!("color mismatch: expected Single"),
228        }
229    }
230
231    /// Adds a new line of text to a specific column within a specific columnar segment.
232    ///
233    /// This mirrors [`add_text_line_indx`](Self::add_text_line_indx), but targets a single
234    /// column inside a columnar segment created via
235    /// [`add_col_text_sgmt`](Self::add_col_text_sgmt). Each column accumulates its own
236    /// independent list of lines, stacked top-to-bottom within that column.
237    ///
238    /// # Arguments
239    ///
240    /// * `data_string` - The text content to add
241    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
242    /// * `seg_index` - The index of the columnar segment to add this line to (0-based)
243    /// * `col_index` - The index of the column within that segment to add this line to (0-based)
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use boxy_cli::prelude::*;
249    ///
250    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
251    /// my_box.add_col_text_sgmt(BoxAlign::Left, 2);
252    ///
253    /// // Add a line to column 0, then another to column 1
254    /// my_box.add_col_text_line_indx("Name: Alice", "#ffffff", &0usize, &0usize);
255    /// my_box.add_col_text_line_indx("Name: Bob", "#ffffff", &0usize, &1usize);
256    /// ```
257    ///
258    /// # Note
259    ///
260    /// If columns within the same segment have different numbers of lines, shorter columns
261    /// are padded with blank rows to match the height of the tallest one. This happens
262    /// automatically at render time — you do not need to add blank lines manually.
263    ///
264    /// # Panics
265    ///
266    /// Panics if:
267    /// - `seg_index` is out of bounds
268    /// - The segment at `seg_index` is not a columnar segment
269    /// - `col_index` is out of bounds for that segment's column count
270    pub fn add_col_text_line_indx(
271        &mut self,
272        data_string: &str,
273        color: &str,
274        seg_index: &usize,
275        col_index: &usize,
276    ) {
277        match &mut self.data[*seg_index] {
278            SegType::Single(_) => {
279                panic!("Failed to add columnar text data to SegType::Single segment!")
280            }
281            SegType::Columnar(data) => {
282                if *col_index >= self.seg_cols_count[*seg_index] {
283                    panic!("failed to add columnar data: INVALID COLUMN INDEX");
284                }
285                data[*col_index].push(Cow::from(data_string.to_owned()));
286            }
287        }
288        match &mut self.colors[*seg_index] {
289            SegColor::Columnar(cols) => cols[*col_index].push(SegColor::parse_hexcolor(color)),
290            SegColor::Single(_) => panic!(
291                "colors shape mismatch: a columnar data segment should always have columnar colors"
292            ),
293        }
294    }
295
296    /// Adds a new line of text to the most recently added segment.
297    ///
298    /// This is a convenience method that adds a line to the last segment created,
299    /// eliminating the need to specify the segment index. The new line appears below
300    /// existing content in that segment with no divider between them.
301    ///
302    /// # Arguments
303    ///
304    /// * `data_string` - The text content to add
305    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
306    ///
307    /// # Examples
308    ///
309    /// ```
310    /// use boxy_cli::prelude::*;
311    ///
312    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
313    /// my_box.add_text_sgmt("Header", "#ffffff", BoxAlign::Center);
314    /// my_box.add_text_line("Additional details below the header", "#32CD32");
315    /// ```
316    ///
317    /// # Panics
318    ///
319    /// Panics if no segments have been added yet, or if the last segment is a columnar
320    /// segment — use [`add_col_text_line`](Self::add_col_text_line) for those.
321    pub fn add_text_line(&mut self, data_string: &str, color: &str) {
322        match &mut self.data[self.sect_count - 1] {
323            SegType::Single(lines) => lines.push(Cow::from(data_string.to_owned())),
324            SegType::Columnar(_) => panic!("add_text_line_indx called on Columnar segment!"),
325        }
326        match &mut self.colors[self.sect_count - 1] {
327            SegColor::Single(cols) => cols.push(SegColor::parse_hexcolor(color)),
328            SegColor::Columnar(_) => panic!("color mismatch: expected Single"),
329        }
330    }
331
332    /// Adds a new line of text to a specific column within the most recently added segment.
333    ///
334    /// This is a convenience method that mirrors [`add_text_line`](Self::add_text_line), but
335    /// for columnar segments: it adds a line to a column of the last segment that was created,
336    /// eliminating the need to specify the segment index.
337    ///
338    /// # Arguments
339    ///
340    /// * `data_string` - The text content to add
341    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
342    /// * `col_index` - The index of the column within the last segment to add this line to (0-based)
343    ///
344    /// # Examples
345    ///
346    /// ```
347    /// use boxy_cli::prelude::*;
348    ///
349    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
350    /// my_box.add_col_text_sgmt(BoxAlign::Left, 2);
351    /// my_box.add_col_text_line("Left column text", "#ffffff", &0usize);
352    /// my_box.add_col_text_line("Right column text", "#ffffff", &1usize);
353    /// ```
354    ///
355    /// # Note
356    ///
357    /// If columns within the same segment have different numbers of lines, shorter columns
358    /// are padded with blank rows to match the height of the tallest one. This happens
359    /// automatically at render time — you do not need to add blank lines manually.
360    ///
361    /// # Panics
362    ///
363    /// Panics if no segments have been added yet, if the last segment is not a columnar
364    /// segment, or if `col_index` is out of bounds for that segment's column count.
365    pub fn add_col_text_line(&mut self, data_string: &str, color: &str, col_index: &usize) {
366        let seg_index = self.sect_count - 1;
367        self.add_col_text_line_indx(data_string, color, &seg_index, col_index);
368    }
369
370    /// Sets the overall alignment of the box within the terminal.
371    ///
372    /// This controls where the box is positioned horizontally on screen,
373    /// not the alignment of text inside it (which is set per-segment).
374    ///
375    /// # Behaviour with external padding
376    ///
377    /// When set to [`BoxAlign::Center`], the box is positioned at the true
378    /// center of the terminal. External left/right padding is still used to
379    /// determine the box width (more padding → narrower box), but the resulting
380    /// box is always centerd — the padding values do not shift it left or right.
381    /// As long as the terminal is wide enough, external padding is effectively
382    /// a width constraint rather than a margin.
383    ///
384    /// # Arguments
385    ///
386    /// * `align` - The alignment to use: `BoxAlign::Left`, `BoxAlign::Center`, or `BoxAlign::Right`
387    ///
388    /// # Examples
389    ///
390    /// ```
391    /// use boxy_cli::prelude::*;
392    ///
393    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
394    /// my_box.set_align(BoxAlign::Center); // center the box in the terminal
395    /// ```
396    ///
397    /// ```
398    /// use boxy_cli::prelude::*;
399    ///
400    /// // External padding shrinks the box but does not shift it off-center
401    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
402    /// my_box.set_align(BoxAlign::Center);
403    /// my_box.set_ext_padding(BoxPad::uniform(5)); // box is 10 chars narrower, still centerd
404    /// ```
405    pub fn set_align(&mut self, align: BoxAlign) {
406        self.align = align;
407    }
408
409    /// Sets the internal padding between the text box border and its text content.
410    ///
411    /// Internal padding creates space between the border of the box and the text inside it.
412    ///
413    /// # Arguments
414    ///
415    /// * `int_padding` - A `BoxPad` instance specifying the padding values
416    ///
417    /// # Examples
418    ///
419    /// ```
420    /// use boxy_cli::prelude::*;
421    ///
422    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
423    ///
424    /// // Set uniform padding of 2 spaces on all sides
425    /// my_box.set_int_padding(BoxPad::uniform(2));
426    ///
427    /// // Or set different padding for each side (top, left, down, right)
428    /// my_box.set_int_padding(BoxPad::from_tldr(1, 3, 1, 3));
429    /// ```
430    pub fn set_int_padding(&mut self, int_padding: BoxPad) {
431        self.int_padding = int_padding;
432    }
433    /// Sets the external padding between the terminal edges and the text box.
434    ///
435    /// External padding creates space between the terminal edge and the box border,
436    /// which affects both positioning (for [`BoxAlign::Left`] and [`BoxAlign::Right`])
437    /// and box width.
438    ///
439    /// # Behaviour with center alignment
440    ///
441    /// When the box alignment is [`BoxAlign::Center`], left and right external padding
442    /// values affect the **width** of the box (larger padding → narrower box) but do
443    /// not shift its position. The box always occupies the center of the terminal
444    /// regardless of the padding values set here, as long as the terminal is wide enough.
445    /// Top and bottom padding always behave as blank lines regardless of alignment.
446    ///
447    /// # Arguments
448    ///
449    /// * `ext_padding` - A [`BoxPad`] instance specifying the padding values
450    ///
451    /// # Examples
452    ///
453    /// ```
454    /// use boxy_cli::prelude::*;
455    ///
456    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
457    /// my_box.set_ext_padding(BoxPad::uniform(5));
458    /// ```
459    ///
460    /// ```
461    /// use boxy_cli::prelude::*;
462    ///
463    /// // With center alignment, padding shrinks the box but keeps it centerd
464    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
465    /// my_box.set_align(BoxAlign::Center);
466    /// my_box.set_ext_padding(BoxPad::from_tldr(1, 10, 1, 10)); // 20 chars narrower, still centerd
467    /// ```
468    pub fn set_ext_padding(&mut self, ext_padding: BoxPad) {
469        self.ext_padding = ext_padding;
470    }
471    /// Sets both internal and external padding for the text box in a single call.
472    ///
473    /// This is a convenience method that combines `set_int_padding` and `set_ext_padding`.
474    ///
475    /// /// # Note
476    ///
477    /// See [`set_align`](Self::set_align) for how external padding interacts
478    /// with [`BoxAlign::Center`].
479    ///
480    /// # Arguments
481    ///
482    /// * `ext_padding` - A `BoxPad` instance for the external padding (between terminal edges and box)
483    /// * `int_padding` - A `BoxPad` instance for the internal padding (between box border and text)
484    ///
485    /// # Examples
486    ///
487    /// ```
488    /// use boxy_cli::prelude::*;
489    ///
490    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
491    ///
492    /// // Set both internal and external padding
493    /// my_box.set_padding(
494    ///     BoxPad::from_tldr(1, 5, 1, 5), // external padding
495    ///     BoxPad::uniform(2)            // internal padding
496    /// );
497    /// ```
498    pub fn set_padding(&mut self, ext_padding: BoxPad, int_padding: BoxPad) {
499        self.int_padding = int_padding;
500        self.ext_padding = ext_padding;
501    }
502
503    /// Sets a fixed width for the box instead of dynamically sizing to the terminal.
504    ///
505    /// The `width` value includes the two border characters, so the usable inner text
506    /// area is `width - 2` columns (minus any internal padding on top of that). Setting
507    /// `width` to `0` returns to dynamic terminal-width sizing.
508    ///
509    /// # Arguments
510    ///
511    /// * `width` - Total box width in terminal columns, including border characters
512    ///
513    /// # Examples
514    ///
515    /// ```
516    /// use boxy_cli::prelude::*;
517    ///
518    /// let mut b = Boxy::new(BoxType::Single, "#00ffff");
519    /// b.set_width(60); // 60 total columns: 2 borders + 58 usable
520    /// b.add_text_sgmt("Fixed width box", "#ffffff", BoxAlign::Center);
521    /// b.display();
522    /// ```
523    pub fn set_width(&mut self, width: usize) {
524        self.fixed_width = width;
525    }
526
527    /// Sets a fixed height for the text box by adding whitespace above and below the text.
528    ///
529    /// # Arguments
530    ///
531    /// * `height` - The desired height in characters (including borders)
532    ///
533    /// # Examples
534    ///
535    /// ```
536    /// use boxy_cli::prelude::*;
537    ///
538    /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
539    /// my_box.set_height(20); // Set box height to 20 lines
540    /// ```
541    ///
542    /// # Note
543    ///
544    /// This feature is experimental and may not work as expected in the current version.
545    /// Setting height to 0 returns to dynamic sizing based on content.
546    pub fn set_height(&mut self, height: usize) {
547        self.fixed_height = height;
548    }
549
550    /// Sets the border style for the box.
551    ///
552    /// Can be called at any point, including after segments have been added. Takes effect
553    /// on the next call to [`display`](Self::display).
554    ///
555    /// # Arguments
556    ///
557    /// * `box_type` - The border style from the [`BoxType`] enum
558    ///
559    /// # Examples
560    ///
561    /// ```
562    /// use boxy_cli::prelude::*;
563    ///
564    /// let mut b = Boxy::new(BoxType::Single, "#00ffff");
565    /// b.add_text_sgmt("Hello", "#ffffff", BoxAlign::Center);
566    /// b.set_type(BoxType::Double); // switch to double borders before displaying
567    /// b.display();
568    /// ```
569    pub fn set_type(&mut self, box_type: BoxType) {
570        self.type_enum = box_type;
571    }
572
573    /// Sets the border color using a hex color code.
574    ///
575    /// Can be called at any point before [`display`](Self::display). On an invalid hex
576    /// string, falls back to white and prints a warning to stderr.
577    ///
578    /// # Arguments
579    ///
580    /// * `color` - Hex color code (e.g. `\"#ffffff\"`). Falls back to white with a stderr warning on invalid input
581    ///
582    /// # Examples
583    ///
584    /// ```
585    /// use boxy_cli::prelude::*;
586    ///
587    /// let mut b = Boxy::new(BoxType::Single, "#00ffff");
588    /// b.set_color("#ff0000"); // change border to red
589    /// ```
590    pub fn set_color(&mut self, color: &str) {
591        self.box_col = SegColor::parse_hexcolor(color);
592    }
593
594    /// Sets the column width ratios for a columnar segment.
595    ///
596    /// Ratios are relative — `vec![1, 2, 1]` gives the middle column twice the width
597    /// of the others. The number of ratios must exactly match the column count the
598    /// segment was created with.
599    ///
600    /// If this is never called, columns default to equal widths (equivalent to
601    /// `vec![1; column_count]`).
602    ///
603    /// # Arguments
604    ///
605    /// * `seg_index` - Zero-based index of the columnar segment to configure
606    /// * `ratios` - One ratio value per column; values are relative, not absolute
607    ///
608    /// # Panics
609    ///
610    /// Panics if:
611    /// - `seg_index` is out of bounds
612    /// - The segment at `seg_index` is a `Single` text segment, not columnar
613    /// - The length of `ratios` does not match the column count of the segment
614    ///
615    /// # Examples
616    ///
617    /// ```
618    /// use boxy_cli::prelude::*;
619    ///
620    /// let mut b = Boxy::new(BoxType::Single, "#00ffff");
621    /// b.add_col_text_sgmt(BoxAlign::Left, 3);
622    /// // Give the last column twice the space of the first two
623    /// b.set_segment_ratios(0, vec![1, 1, 2]);
624    /// ```
625    pub fn set_segment_ratios(&mut self, seg_index: usize, ratios: Vec<usize>) {
626        assert!(
627            seg_index < self.data.len(),
628            "set_segment_ratios: seg_index {} is out of bounds ({} segments exist)",
629            seg_index,
630            self.data.len()
631        );
632        assert!(
633            matches!(self.data[seg_index], SegType::Columnar(_)),
634            "set_segment_ratios: segment {} is not a columnar segment",
635            seg_index
636        );
637        assert_eq!(
638            ratios.len(),
639            self.seg_cols_count[seg_index],
640            "set_segment_ratios: segment {} has {} columns, but {} ratios were given",
641            seg_index,
642            self.seg_cols_count[seg_index],
643            ratios.len()
644        );
645        self.seg_cols_ratio[seg_index] = ratios;
646    }
647
648    /// Renders and displays the text box in the terminal.
649    ///
650    /// Automatically sizes the box to the current terminal width unless a fixed width
651    /// has been set via [`set_width`](Self::set_width). Call this after all segments
652    /// and configuration are set — subsequent calls re-render with the current terminal
653    /// size, so the box will adapt if the terminal was resized between calls.
654    ///
655    /// Output uses ANSI true-color escape codes. Terminals without true-color support
656    /// will fall back gracefully to the nearest available color via the `colored` crate.
657    /// On terminals with `NO_COLOR` set or where color is disabled, plain text is emitted.
658    ///
659    /// # Examples
660    ///
661    /// ```
662    /// use boxy_cli::prelude::*;
663    ///
664    /// let mut my_box = Boxy::new(BoxType::Double, "#00ffff");
665    /// my_box.add_text_sgmt("Hello, World!", "#ffffff", BoxAlign::Center);
666    /// my_box.display();
667    /// ```
668    pub fn display(&mut self) {
669        // Initializing Display Variables
670
671        let term_size = match termsize::get() {
672            Some(s) => s.cols as usize,
673            None => {
674                // no tty, so just dunp raw text, no need to pollute stream with pipes and dividers
675                for seg in &self.data {
676                    match seg {
677                        SegType::Single(lines) => {
678                            for line in lines {
679                                println!("{}", line.trim());
680                            }
681                        }
682                        SegType::Columnar(cols) => {
683                            for col in cols {
684                                for line in col {
685                                    println!("{}", line.trim());
686                                }
687                            }
688                        }
689                    }
690                }
691                return;
692            }
693        };
694
695        // Fix width to accommodate for box characters
696        let disp_width = if self.fixed_width != 0 {
697            self.fixed_width - 2
698        } else {
699            term_size
700                .saturating_sub(self.ext_padding.lr())
701                .saturating_sub(2)
702                .max(1)
703        };
704
705        // Parse box color only once per display
706        let box_col_truecolor = self.box_col;
707        // Resolve template once per display
708        let box_pieces = map_box_type(&self.type_enum);
709        // get alignment-based offset
710        let align_offset = align_offset(&disp_width, &term_size, &self.align, &self.ext_padding);
711
712        // pre-emptively get the dividers map:
713        let mut col_widths_segwise: Vec<Vec<usize>> = Vec::new();
714        for i in 0..self.sect_count {
715            if let SegType::Single(_) = self.data[i] {
716                col_widths_segwise.push(Vec::new());
717            } else {
718                col_widths_segwise.push(self.col_widths(&i, &disp_width));
719            }
720        }
721
722        // Printing the top segment
723        let mut top_seg: String = String::new();
724        match self.data.first() {
725            None | Some(&SegType::Single(_)) => {
726                write!(
727                    top_seg,
728                    "{:>width$}",
729                    box_pieces.top_left,
730                    width = self.ext_padding.left + align_offset
731                )
732                .unwrap();
733                top_seg.push_str(&box_pieces.horizontal.to_string().repeat(disp_width));
734                top_seg.push(box_pieces.top_right);
735            }
736            Some(&SegType::Columnar(_)) => {
737                write!(
738                    top_seg,
739                    "{:>width$}",
740                    box_pieces.top_left,
741                    width = self.ext_padding.left + align_offset
742                )
743                .unwrap();
744                let below = self.col_boundaries(&col_widths_segwise[0]);
745                for i in 0..disp_width {
746                    match below.contains(&i) {
747                        true => {
748                            top_seg.push(box_pieces.upper_t);
749                        }
750                        false => {
751                            top_seg.push(box_pieces.horizontal);
752                        }
753                    };
754                }
755                top_seg.push(box_pieces.top_right);
756            }
757        }
758        println!("{}", top_seg.color(box_col_truecolor));
759
760        // Iteratively print all the textbox sections, with appropriate dividers in between
761        for i in 0..self.sect_count {
762            if i > 0 {
763                self.print_h_divider(
764                    &box_col_truecolor,
765                    disp_width,
766                    align_offset,
767                    &box_pieces,
768                    &col_widths_segwise.get(i - 1),
769                    &col_widths_segwise.get(i),
770                );
771            }
772            if let SegType::Single(_) = self.data[i] {
773                self.display_segment(i, disp_width, align_offset, &box_pieces, &box_col_truecolor);
774            } else {
775                self.print_cols(
776                    i,
777                    align_offset,
778                    &box_pieces,
779                    &box_col_truecolor,
780                    &col_widths_segwise[i],
781                );
782            }
783        }
784
785        // Printing the bottom segment
786        let mut bot_seg: String = String::new();
787        match self.data.last() {
788            None | Some(&SegType::Single(_)) => {
789                write!(
790                    bot_seg,
791                    "{:>width$}",
792                    box_pieces.bottom_left,
793                    width = self.ext_padding.left + align_offset
794                )
795                .unwrap();
796                bot_seg.push_str(&box_pieces.horizontal.to_string().repeat(disp_width));
797                bot_seg.push(box_pieces.bottom_right);
798            }
799            Some(&SegType::Columnar(_)) => {
800                write!(
801                    bot_seg,
802                    "{:>width$}",
803                    box_pieces.bottom_left,
804                    width = self.ext_padding.left + align_offset
805                )
806                .unwrap();
807                let above = self.col_boundaries(
808                    &col_widths_segwise
809                        .last()
810                        .expect("failed to get last element"),
811                );
812                for i in 0..disp_width {
813                    match above.contains(&i) {
814                        true => {
815                            bot_seg.push(box_pieces.lower_t);
816                        }
817                        false => {
818                            bot_seg.push(box_pieces.horizontal);
819                        }
820                    };
821                }
822                bot_seg.push(box_pieces.bottom_right);
823            }
824        }
825        println!("{}", bot_seg.color(box_col_truecolor));
826    }
827
828    // Displaying each segment body
829    fn display_segment(
830        &mut self,
831        seg_index: usize,
832        disp_width: usize,
833        align_offset: usize,
834        box_pieces: &BoxTemplates,
835        box_col_truecolor: &Color,
836    ) {
837        let lines = match &self.data[seg_index] {
838            SegType::Single(lines) => lines,
839            SegType::Columnar(_) => return,
840        };
841
842        // Loop for all text lines
843        for i in 0..lines.len() {
844            // obtaining text colour truevalues
845            let text_col_truecolor = match &self.colors[seg_index] {
846                SegColor::Single(cols) => cols[i],
847                SegColor::Columnar(_) => Color::White, // shouldn't happen in display_segment
848            };
849            // Processing data
850            let processed_data = lines[i].trim().to_owned() + " ";
851
852            let liner: Vec<String> =
853                text_wrap_vec_fast(&processed_data, disp_width, &self.int_padding);
854
855            // Generating new External Pad based on alignment offset
856            let ext_offset = BoxPad {
857                top: self.ext_padding.top,
858                left: self.ext_padding.left + align_offset,
859                right: self.ext_padding.right,
860                down: self.ext_padding.down,
861            };
862
863            // Actually printing shiet
864
865            // Iterative printing. Migrated from recursive to prevent stack overflows with larger text bodies and reduce complexity,
866            // also to improve code efficiency
867            iter_line_prnt(
868                &liner,
869                box_pieces,
870                box_col_truecolor,
871                &text_col_truecolor,
872                (&disp_width, &(self.fixed_width != 0)),
873                (&ext_offset, &self.int_padding),
874                &self.seg_align[seg_index],
875            );
876
877            // printing an empty line between consecutive non-terminal text line
878            if i < lines.len() - 1 {
879                println!(
880                    "{1:>width$}{}{1}",
881                    " ".repeat(disp_width),
882                    box_pieces.vertical.to_string().color(*box_col_truecolor),
883                    width = self.ext_padding.left + align_offset
884                );
885            }
886        }
887        // Recursive Printing of text -> now depreciated
888        // recur_whitespace_printing(&processed_data, &mut ws_indices, &self.type_enum, &terminal_size, 0usize, &col_truevals, &self.ext_padding, &self.int_padding, &self.align);
889    }
890
891    // Printing the horizontal divider. - I don't think this is needed?
892    fn print_h_divider(
893        &self,
894        box_col_truecolor: &Color,
895        disp_width: usize,
896        align_offset: usize,
897        box_pieces: &BoxTemplates,
898        prev_seg: &Option<&Vec<usize>>,
899        next_seg: &Option<&Vec<usize>>,
900    ) {
901        // push left segment
902        let mut div: String = String::new();
903        write!(
904            div,
905            "{:>width$}",
906            box_pieces.left_t.to_string(),
907            width = self.ext_padding.left + align_offset
908        )
909        .unwrap();
910        let empty = Vec::new();
911        let above = self.col_boundaries(prev_seg.unwrap_or(&empty));
912        let below = self.col_boundaries(next_seg.unwrap_or(&empty));
913        for i in 0..disp_width {
914            let ch = match (above.contains(&i), below.contains(&i)) {
915                (true, true) => box_pieces.cross,
916                (false, true) => box_pieces.upper_t,
917                (true, false) => box_pieces.lower_t,
918                (false, false) => box_pieces.horizontal,
919            };
920            div.push(ch);
921        }
922        // push right segment
923        div.push(box_pieces.right_t);
924
925        // print this shit
926        println!("{}", div.color(*box_col_truecolor));
927    }
928
929    fn col_widths(&self, seg_index: &usize, disp_width: &usize) -> Vec<usize> {
930        let col_count = self.seg_cols_count[*seg_index];
931        let total_width_ratio: usize = self.seg_cols_ratio[*seg_index].iter().sum();
932        // accommodate for the vertical dividers between the segments
933        let printable =
934            disp_width.saturating_sub(self.seg_cols_count[*seg_index].saturating_sub(1));
935        // get final terminal width ratios -> divide with floor, whatever's left goes to last segment
936        let mut col_seg_widths: Vec<usize> = Vec::new();
937        let mut allocated = 0usize;
938        // iteratively allocate column widths (w/o dividers, i.e. pure text printing areas)
939        for (i, ratio) in self.seg_cols_ratio[*seg_index].iter().enumerate() {
940            let width = if i == col_count - 1 {
941                printable.saturating_sub(allocated) // saturating_sub to prevent underflow panics
942            } else {
943                ((*ratio as f64 / total_width_ratio as f64) * printable as f64).floor() as usize
944            };
945            allocated += width;
946            col_seg_widths.push(width);
947        } // ^^^ a little complicated, but will work on improving it ^^^
948        col_seg_widths
949    }
950
951    fn col_boundaries(&self, col_widths: &Vec<usize>) -> Vec<usize> {
952        let mut boundaries: Vec<usize> = Vec::with_capacity(col_widths.len());
953        let mut x = 0;
954        for (i, w) in col_widths.iter().enumerate() {
955            x += w;
956            if i < col_widths.len() - 1 {
957                boundaries.push(x);
958                x += 1;
959            }
960        }
961        boundaries
962    }
963
964    fn print_cols(
965        &self,
966        seg_index: usize,
967        align_offset: usize,
968        box_pieces: &BoxTemplates,
969        box_col_truecolor: &Color,
970        col_seg_widths: &Vec<usize>,
971    ) {
972        let col_count = self.seg_cols_count[seg_index];
973
974        let mut columnar_data: Vec<Vec<(String, Color)>> = Vec::new();
975        let mut col_height_max = 0;
976        for i in 0..col_count {
977            let col_data = match &self.data[seg_index] {
978                SegType::Columnar(cols) => &cols[i],
979                SegType::Single(_) => return,
980            };
981            let col_colors = match &self.colors[seg_index] {
982                SegColor::Columnar(cols) => &cols[i],
983                SegColor::Single(_) => return,
984            };
985            let mut col_wrapped: Vec<(String, Color)> = Vec::new();
986            for (line_idx, line) in col_data.iter().enumerate() {
987                // obtaining text colour truevalue for this line, falling back to white on
988                // a missing/unparseable color (mirrors display_segment's handling)
989                let text_col_truecolor = col_colors.get(line_idx).copied().unwrap_or(Color::White);
990                for wrapped_line in text_wrap_vec_fast(
991                    line.as_ref(),
992                    col_seg_widths[i],
993                    &DEFAULT_PAD, // keep the standard, default padding
994                ) {
995                    col_wrapped.push((wrapped_line, text_col_truecolor));
996                }
997            }
998            col_height_max = col_height_max.max(col_wrapped.len());
999            columnar_data.push(col_wrapped);
1000        }
1001
1002        let vertical = box_pieces.vertical.to_string().color(*box_col_truecolor);
1003
1004        for curr_line in 0..col_height_max {
1005            let mut currline = String::new();
1006            write!(
1007                currline,
1008                "{:>width$}",
1009                vertical,
1010                width = self.ext_padding.left + align_offset
1011            )
1012            .unwrap();
1013            for (i, col) in columnar_data.iter().enumerate() {
1014                if i > 0 {
1015                    write!(currline, "{}", vertical).unwrap();
1016                }
1017                let width = col_seg_widths[i].saturating_sub(1);
1018                match col.get(curr_line) {
1019                    Some((content, color)) => {
1020                        write!(
1021                            currline,
1022                            " {:<width$}",
1023                            content.color(*color),
1024                            width = width
1025                        )
1026                        .unwrap();
1027                    }
1028                    None => {
1029                        write!(currline, " {:<width$}", "", width = width).unwrap();
1030                    }
1031                }
1032            }
1033            write!(currline, "{}", vertical).unwrap();
1034            println!("{}", currline);
1035        }
1036    }
1037}
1038
1039// Faster non-allocating whitespace scanning text wrapper
1040// Returns wrapped text, line by line in a vec
1041#[doc(hidden)]
1042fn text_wrap_vec_fast(data: &str, disp_width: usize, int_padding: &BoxPad) -> Vec<String> {
1043    let mut liner: Vec<String> = Vec::new();
1044    let max_len = disp_width.saturating_sub(int_padding.lr() + 2);
1045    if max_len == 0 {
1046        return liner;
1047    }
1048    let bytes = data.as_bytes();
1049    let mut start = 0usize;
1050    while start < data.len() {
1051        let mut end = (start + max_len).min(data.len());
1052        if end < data.len() {
1053            let mut last_space: Option<usize> = None;
1054            let mut j = start;
1055            while j < end {
1056                if bytes[j] == b' ' {
1057                    last_space = Some(j);
1058                }
1059                j += 1;
1060            }
1061            if let Some(ws) = last_space {
1062                end = ws;
1063            }
1064        }
1065        liner.push(data[start..end].to_string());
1066        if end >= data.len().saturating_sub(1) {
1067            break;
1068        }
1069        // Advance past space if present to avoid leading spaces on next line
1070        start = if end < data.len() && bytes[end] == b' ' {
1071            end + 1
1072        } else {
1073            end
1074        };
1075    }
1076    liner
1077}
1078
1079#[doc(hidden)]
1080fn iter_line_prnt(
1081    liner: &[String],
1082    box_pieces: &BoxTemplates,
1083    box_col: &Color,
1084    text_col: &Color,
1085    disp_params: (&usize, &bool),
1086    padding: (&BoxPad, &BoxPad),
1087    align: &BoxAlign,
1088) {
1089    // TODO add support for unicode wide characters like glyphs and emojis
1090    let (ext_padding, int_padding) = padding;
1091    let (disp_width, fixed_size) = disp_params;
1092    let printable_area = disp_width - int_padding.lr()
1093        + 2 * ((int_padding.left != 0) as usize) * (!*fixed_size as usize); // IDK why this works, but it does
1094    let vertical = box_pieces.vertical.to_string().color(*box_col);
1095    match align {
1096        BoxAlign::Left => {
1097            for i in liner.iter() {
1098                let mut currline = String::new();
1099                write!(currline, "{:>width$}", vertical, width = ext_padding.left).unwrap();
1100                write!(currline, "{:<pad$}", " ", pad = int_padding.left).unwrap();
1101                write!(
1102                    currline,
1103                    "{:<width$}",
1104                    i.color(*text_col),
1105                    width = printable_area - (2 * (!(*fixed_size) as usize)) // subtract 2 for the bars if on dynamic sizing
1106                )
1107                .unwrap();
1108                write!(currline, "{:<pad$}", " ", pad = int_padding.right).unwrap();
1109                write!(currline, "{}", vertical).unwrap();
1110                println!("{}", currline);
1111            }
1112        }
1113        BoxAlign::Center => {
1114            for i in liner.iter() {
1115                let mut currline = String::new();
1116                write!(currline, "{:>width$}", vertical, width = ext_padding.left).unwrap();
1117                write!(
1118                    currline,
1119                    "{:<pad$}",
1120                    " ",
1121                    pad = int_padding.left + ((printable_area - i.len()) / 2)
1122                )
1123                .unwrap();
1124                write!(currline, "{}", i.color(*text_col)).unwrap();
1125                write!(
1126                    currline,
1127                    "{:<pad$}",
1128                    " ",
1129                    pad = int_padding.right + (printable_area - i.len())
1130                        - ((printable_area - i.len()) / 2)
1131                        - (2 * (int_padding.right != 0) as usize) // sub 2 if doing internal padding
1132                        + (2 * (*fixed_size as usize)) // add 2 if going by fixed size; if doing fixed with pad, do nothing
1133                )
1134                .unwrap();
1135                write!(currline, "{}", vertical).unwrap();
1136                println!("{}", currline);
1137            }
1138        }
1139        BoxAlign::Right => {
1140            for i in liner.iter() {
1141                let mut currline = String::new();
1142                write!(currline, "{:>width$}", vertical, width = ext_padding.left).unwrap();
1143                write!(currline, "{:<pad$}", " ", pad = int_padding.left).unwrap();
1144                write!(
1145                    currline,
1146                    "{:>width$}",
1147                    i.color(*text_col),
1148                    width = printable_area - (2 * (!*fixed_size as usize)) // subtract 2 for the bars if on dynamic sizing
1149                )
1150                .unwrap();
1151                write!(currline, "{:<pad$}", " ", pad = int_padding.right).unwrap();
1152                write!(currline, "{}", vertical).unwrap();
1153                println!("{}", currline)
1154            }
1155        }
1156    }
1157}
1158
1159// returns the box template for the given enum
1160#[doc(hidden)]
1161fn map_box_type(boxtype: &BoxType) -> BoxTemplates {
1162    match boxtype {
1163        BoxType::Classic => CLASSIC_TEMPLATE,
1164        BoxType::Single => SINGLE_TEMPLATE,
1165        BoxType::DoubleHorizontal => DOUB_H_TEMPLATE,
1166        BoxType::DoubleVertical => DOUB_V_TEMPLATE,
1167        BoxType::Double => DOUBLE_TEMPLATE,
1168        BoxType::Bold => BOLD_TEMPLATE,
1169        BoxType::Rounded => ROUNDED_TEMPLATE,
1170        BoxType::BoldCorners => BOLD_CORNERS_TEMPLATE,
1171        BoxType::Empty => EMPTY_TEMPLATE,
1172    }
1173}
1174
1175#[doc(hidden)]
1176fn align_offset(
1177    disp_width: &usize,
1178    term_size: &usize,
1179    align: &BoxAlign,
1180    padding: &BoxPad,
1181) -> usize {
1182    match *align {
1183        BoxAlign::Left => 0,
1184        BoxAlign::Center => (term_size - disp_width) / 2 - padding.left,
1185        BoxAlign::Right => term_size - (disp_width + 2 * padding.right + padding.left),
1186    }
1187}
1188
1189// Macro type resolution functions for boxy!
1190
1191// These helpers are public so the macro can access them across crate boundaries via $crate::boxer::...
1192// They are hidden from docs and not intended for direct user consumption.
1193#[doc(hidden)]
1194#[allow(dead_code)]
1195pub fn resolve_col(dat: String) -> String {
1196    dat
1197}
1198// Macro type-resolution function
1199#[doc(hidden)]
1200#[allow(dead_code)]
1201pub fn resolve_pad(dat: String) -> BoxPad {
1202    BoxPad::uniform(dat.parse::<usize>().unwrap_or(0usize))
1203}
1204// Macro type-resolution function
1205#[doc(hidden)]
1206#[allow(dead_code)]
1207pub fn resolve_align(dat: String) -> BoxAlign {
1208    match &*dat {
1209        "center" => BoxAlign::Center,
1210        "right" => BoxAlign::Right,
1211        "left" => BoxAlign::Left,
1212        _ => BoxAlign::Left,
1213    }
1214}
1215// Macro type-resolution function
1216#[doc(hidden)]
1217#[allow(dead_code)]
1218pub fn resolve_type(dat: String) -> BoxType {
1219    match &*dat {
1220        "classic" | "c" => BoxType::Classic,
1221        "single" | "s" => BoxType::Single,
1222        "double_horizontal" | "dh" => BoxType::DoubleHorizontal,
1223        "double_vertical" | "dv" => BoxType::DoubleVertical,
1224        "double" | "d" => BoxType::Double,
1225        "bold" | "b" => BoxType::Bold,
1226        "rounded" | "r" => BoxType::Rounded,
1227        "bold_corners" | "bc" => BoxType::BoldCorners,
1228        "empty" | "e" => BoxType::Empty,
1229        _ => BoxType::Single,
1230    }
1231}
1232// Macro type-resolution function
1233#[doc(hidden)]
1234#[allow(dead_code)]
1235pub fn resolve_segments(dat: String) -> usize {
1236    dat.parse().expect("failed to parse total segment number")
1237}
1238
1239// Builder
1240/// The BoxyBuilder struct implements a fluent builder pattern for creating `Boxy` instances.
1241///
1242/// This builder provides a more expressive and readable way to create and configure text boxes.
1243/// Each method returns the builder instance itself, allowing method calls to be chained together.
1244/// When the configuration is complete, call the `build()` method to create the actual [`Boxy`](./struct.Boxy.html) instance.
1245///
1246/// # Examples
1247///
1248/// ```
1249/// use boxy_cli::prelude::*;
1250///
1251/// // Create and display a text box in a single fluent sequence
1252/// Boxy::builder()
1253///     .box_type(BoxType::Double)
1254///     .color("#00ffff")
1255///     .padding(BoxPad::uniform(1), BoxPad::from_tldr(2, 2, 1, 1))
1256///     .align(BoxAlign::Center)
1257///     .add_segment("Hello, Boxy!", "#ffffff", BoxAlign::Center)
1258///     .add_line("This is a new line.", "#32CD32")
1259///     .add_segment("Another section", "#663399", BoxAlign::Left)
1260///     .width(50)
1261///     .build()
1262///     .display();
1263/// ```
1264#[derive(Debug)]
1265pub struct BoxyBuilder<'a> {
1266    type_enum: BoxType,
1267    data: Vec<SegType<'a>>,
1268    box_col: Color,
1269    colors: Vec<SegColor>,
1270    int_padding: BoxPad,
1271    ext_padding: BoxPad,
1272    align: BoxAlign,
1273    seg_align: Vec<BoxAlign>,
1274    fixed_width: usize,
1275    fixed_height: usize,
1276    seg_cols_ratio: Vec<Vec<usize>>,
1277    terminal_width_offset: i32,
1278    seg_col_count: Vec<usize>,
1279}
1280
1281impl<'a> BoxyBuilder<'a> {
1282    fn default() -> Self {
1283        Self {
1284            type_enum: BoxType::Single,
1285            data: Vec::new(),
1286            box_col: Color::White,
1287            colors: Vec::new(),
1288            int_padding: BoxPad::new(),
1289            ext_padding: BoxPad::new(),
1290            align: BoxAlign::Left,
1291            seg_align: Vec::new(),
1292            fixed_width: 0,
1293            fixed_height: 0,
1294            seg_cols_ratio: Vec::new(),
1295            terminal_width_offset: -20,
1296            seg_col_count: Vec::new(),
1297        }
1298    }
1299
1300    /// Creates a new `BoxyBuilder` with default values.
1301    ///
1302    /// This creates a builder with the following default values:
1303    /// - Box type: `BoxType::Single`
1304    /// - Color: empty string (will use white if not set)
1305    /// - Padding: zero on all sides
1306    /// - Alignment: `BoxAlign::Left`
1307    /// - No text segments
1308    ///
1309    /// # Examples
1310    ///
1311    /// ```
1312    /// use boxy_cli::prelude::*;
1313    ///
1314    /// let builder = BoxyBuilder::new();
1315    /// // Configure the builder with various methods
1316    /// let my_box = builder.box_type(BoxType::Double)
1317    ///                    .color("#00ffff")
1318    ///                    .build();
1319    /// ```
1320    ///
1321    /// Typically used through the `Boxy::builder()` factory method:
1322    ///
1323    ///
1324    /// ```
1325    /// use boxy_cli::prelude::*;
1326    ///
1327    /// let builder = Boxy::builder(); // Same as BoxyBuilder::new()
1328    /// ```
1329    pub fn new() -> Self {
1330        Self::default()
1331    }
1332
1333    /// Sets the border type for the text box.
1334    ///
1335    /// This determines the visual style of the box borders, including the characters used for
1336    /// corners, edges, and intersections. Different styles can create different visual effects,
1337    /// from simple ASCII-style boxes to double-lined or rounded boxes.
1338    ///
1339    /// # Arguments
1340    ///
1341    /// * `box_type` - The border style from the [`BoxType`](../constructs/enum.BoxType.html) enum
1342    ///
1343    /// # Returns
1344    ///
1345    /// The builder instance for method chaining
1346    ///
1347    /// # Examples
1348    ///
1349    /// ```
1350    /// use boxy_cli::prelude::*;
1351    ///
1352    /// let builder = Boxy::builder()
1353    ///     .box_type(BoxType::Double); // Use double-lined borders
1354    ///
1355    /// // Or try other border styles
1356    /// let rounded_box = Boxy::builder()
1357    ///     .box_type(BoxType::Rounded)
1358    ///     .build();
1359    /// ```
1360    pub fn box_type(mut self, box_type: BoxType) -> Self {
1361        self.type_enum = box_type;
1362        self
1363    }
1364
1365    /// Sets the border color for the text box.
1366    ///
1367    /// This method defines the color of the box borders, including corners, edges, and intersections.
1368    /// The color is specified using a hexadecimal color code (e.g. "#00ffff" for cyan).
1369    ///
1370    /// # Arguments
1371    ///
1372    /// * `box_color` - Hex color code (e.g. `\"#ffffff\"`). Falls back to white with a stderr warning on invalid input
1373    ///
1374    /// # Returns
1375    ///
1376    /// The builder instance for method chaining
1377    ///
1378    /// # Examples
1379    ///
1380    /// ```
1381    /// use boxy_cli::prelude::*;
1382    ///
1383    /// // Create a box with cyan borders
1384    /// let cyan_box = Boxy::builder()
1385    ///     .color("#00ffff")
1386    ///     .build();
1387    ///
1388    /// // Create a box with red borders
1389    /// let red_box = Boxy::builder()
1390    ///     .color("#ff0000")
1391    ///     .build();
1392    /// ```
1393    ///
1394    /// # Note
1395    ///
1396    /// The actual appearance depends on terminal support for colors.
1397    pub fn color(mut self, box_color: &str) -> Self {
1398        self.box_col = SegColor::parse_hexcolor(box_color);
1399        self
1400    }
1401
1402    /// Adds a new text segment to the box with specified text, color, and alignment.
1403    ///
1404    /// Each segment represents a distinct section of the text box that will be separated by
1405    /// horizontal dividers. This method is used to add the first or subsequent major
1406    /// segments of content.
1407    ///
1408    /// # Arguments
1409    ///
1410    /// * `text` - The text content for this segment
1411    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1412    /// * `text_align` - The alignment for this text segment (left, center, right)
1413    ///
1414    /// # Returns
1415    ///
1416    /// The builder instance for method chaining
1417    ///
1418    /// # Examples
1419    ///
1420    /// ```
1421    /// use boxy_cli::prelude::*;
1422    ///
1423    /// let my_box = Boxy::builder()
1424    ///     // Add a centered header segment in white
1425    ///     .add_segment("Header", "#ffffff", BoxAlign::Center)
1426    ///     // Add a left-aligned content segment in green
1427    ///     .add_segment("Content goes here", "#00ff00", BoxAlign::Left)
1428    ///     .build();
1429    /// ```
1430    pub fn add_segment(mut self, text: &str, color: &str, text_align: BoxAlign) -> Self {
1431        self.data
1432            .push(SegType::Single(vec![Cow::from(text.to_owned())]));
1433        self.colors
1434            .push(SegColor::Single(vec![SegColor::parse_hexcolor(color)]));
1435        self.seg_align.push(text_align);
1436        self.seg_col_count.push(0); // Single segment, no columns
1437        self.seg_cols_ratio.push(vec![1]); // placeholder, mirrors add_text_sgmt
1438        self
1439    }
1440
1441    /// Adds a new columnar segment to the box.
1442    /// Adds a new columnar segment to the box with `column_count` side-by-side columns.
1443    ///
1444    /// Columns start empty and are populated with [`add_col_line`](Self::add_col_line) or
1445    /// [`add_col_line_indx`](Self::add_col_line_indx). All columns default to equal width;
1446    /// use [`segment_ratios`](Self::segment_ratios) to customize.
1447    ///
1448    /// # Arguments
1449    ///
1450    /// * `text_align` - Alignment applied to text within each column
1451    /// * `column_count` - Number of columns (must be at least 1)
1452    ///
1453    /// # Returns
1454    ///
1455    /// The builder instance for method chaining
1456    ///
1457    /// # Panics
1458    ///
1459    /// Panics if `column_count` is 0.
1460    ///
1461    /// # Examples
1462    ///
1463    /// ```
1464    /// use boxy_cli::prelude::*;
1465    ///
1466    /// let my_box = Boxy::builder()
1467    ///     .add_col_segment(BoxAlign::Left, 2)
1468    ///     .add_col_line("Left column", "#ffffff", 0)
1469    ///     .add_col_line("Right column", "#ffffff", 1)
1470    ///     .build();
1471    /// ```
1472    pub fn add_col_segment(mut self, text_align: BoxAlign, column_count: usize) -> Self {
1473        assert!(
1474            column_count > 0,
1475            "add_col_segment: column_count must be at least 1"
1476        );
1477        self.data
1478            .push(SegType::Columnar(vec![Vec::new(); column_count]));
1479        self.colors
1480            .push(SegColor::Columnar(vec![Vec::new(); column_count]));
1481        self.seg_align.push(text_align);
1482        self.seg_col_count.push(column_count);
1483        self.seg_cols_ratio.push(vec![1; column_count]); // equal widths by default
1484        self
1485    }
1486
1487    /// Adds a new line of text to the most recently added segment.
1488    ///
1489    /// This method adds a line of text to the last segment that was created.
1490    /// The new line will appear below the existing content in that segment.
1491    /// Unlike `add_segment()`, this does not create a new segment with a divider.
1492    ///
1493    /// # Arguments
1494    ///
1495    /// * `text` - The text content to add as a new line
1496    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1497    ///
1498    /// # Returns
1499    ///
1500    /// The builder instance for method chaining
1501    ///
1502    /// # Examples
1503    ///
1504    /// ```
1505    /// use boxy_cli::prelude::*;
1506    ///
1507    /// let my_box = Boxy::builder()
1508    ///     // Add a header segment
1509    ///     .add_segment("Header", "#ffffff", BoxAlign::Center)
1510    ///     // Add a subheader as a new line in the same segment
1511    ///     .add_line("Subheader text", "#aaaaaa")
1512    ///     // Add a different segment with a divider
1513    ///     .add_segment("Content section", "#00ff00", BoxAlign::Left)
1514    ///     .build();
1515    /// ```
1516    ///
1517    pub fn add_line(mut self, text: &str, color: &str) -> Self {
1518        if let Some(last) = self.data.last_mut() {
1519            match last {
1520                SegType::Single(lines) => lines.push(Cow::from(text.to_owned())),
1521                SegType::Columnar(_) => panic!("add_line called on Columnar segment"),
1522            }
1523            match self
1524                .colors
1525                .last_mut()
1526                .expect("colors out of sync with data")
1527            {
1528                SegColor::Single(cols) => cols.push(SegColor::parse_hexcolor(color)),
1529                SegColor::Columnar(_) => panic!("add_line called on Columnar segment"),
1530            }
1531        } else {
1532            // no segment yet — create one, mirroring add_segment
1533            self.data
1534                .push(SegType::Single(vec![Cow::from(text.to_owned())]));
1535            self.colors
1536                .push(SegColor::Single(vec![SegColor::parse_hexcolor(color)]));
1537            self.seg_align.push(BoxAlign::Left);
1538            self.seg_col_count.push(0);
1539            self.seg_cols_ratio.push(vec![1]);
1540        }
1541        self
1542    }
1543
1544    /// Adds a line of text to a specific column of the most recently added columnar segment.
1545    ///
1546    /// Convenience method — no need to specify the segment index. Mirrors
1547    /// [`add_col_line_indx`](Self::add_col_line_indx) for when you're building top-to-bottom.
1548    ///
1549    /// # Arguments
1550    ///
1551    /// * `text` - The text content to add
1552    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1553    /// * `col_index` - Zero-based index of the column to add this line into
1554    ///
1555    /// # Returns
1556    ///
1557    /// The builder instance for method chaining
1558    ///
1559    /// # Panics
1560    ///
1561    /// Panics if no segment exists, if the last segment is not columnar, or if `col_index`
1562    /// is out of bounds.
1563    ///
1564    /// # Examples
1565    ///
1566    /// ```
1567    /// use boxy_cli::prelude::*;
1568    ///
1569    /// let my_box = Boxy::builder()
1570    ///     .add_col_segment(BoxAlign::Left, 3)
1571    ///     .add_col_line("Name",     "#aaaaaa", 0)
1572    ///     .add_col_line("Status",   "#aaaaaa", 1)
1573    ///     .add_col_line("Notes",    "#aaaaaa", 2)
1574    ///     .add_col_line("Lumio V2", "#ffffff", 0)
1575    ///     .add_col_line("Shipped",  "#32CD32", 1)
1576    ///     .add_col_line("Done",     "#ffffff", 2)
1577    ///     .build();
1578    /// ```
1579    pub fn add_col_line(mut self, text: &str, color: &str, col_index: usize) -> Self {
1580        let seg_index = self.data.len() - 1;
1581        match &mut self.data[seg_index] {
1582            SegType::Columnar(cols) => {
1583                assert!(
1584                    col_index < cols.len(),
1585                    "add_col_line: col_index out of bounds"
1586                );
1587                cols[col_index].push(Cow::from(text.to_owned()));
1588            }
1589            SegType::Single(_) => panic!("add_col_line called on a Single segment"),
1590        }
1591        match &mut self.colors[seg_index] {
1592            SegColor::Columnar(cols) => cols[col_index].push(SegColor::parse_hexcolor(color)),
1593            SegColor::Single(_) => panic!("colors shape mismatch"),
1594        }
1595        self
1596    }
1597
1598    /// Adds a line of text to a specific column of a specific columnar segment by index.
1599    ///
1600    /// Use this when you need to populate segments out of order or return to an earlier
1601    /// segment. For sequential top-to-bottom building, prefer
1602    /// [`add_col_line`](Self::add_col_line).
1603    ///
1604    /// # Arguments
1605    ///
1606    /// * `text` - The text content to add
1607    /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1608    /// * `seg_index` - Zero-based index of the columnar segment
1609    /// * `col_index` - Zero-based index of the column within that segment
1610    ///
1611    /// # Returns
1612    ///
1613    /// The builder instance for method chaining
1614    ///
1615    /// # Panics
1616    ///
1617    /// Panics if `seg_index` is out of bounds, if that segment is not columnar, or if
1618    /// `col_index` is out of bounds for that segment's column count.
1619    ///
1620    /// # Examples
1621    ///
1622    /// ```
1623    /// use boxy_cli::prelude::*;
1624    ///
1625    /// let my_box = Boxy::builder()
1626    ///     .add_segment("Header", "#ffffff", BoxAlign::Center)
1627    ///     .add_col_segment(BoxAlign::Left, 2)
1628    ///     .add_col_line_indx("Left",  "#ffffff", 1, 0)
1629    ///     .add_col_line_indx("Right", "#ffffff", 1, 1)
1630    ///     .build();
1631    /// ```
1632    pub fn add_col_line_indx(
1633        mut self,
1634        text: &str,
1635        color: &str,
1636        seg_index: usize,
1637        col_index: usize,
1638    ) -> Self {
1639        match &mut self.data[seg_index] {
1640            SegType::Columnar(cols) => {
1641                assert!(
1642                    col_index < cols.len(),
1643                    "add_col_line_indx: col_index out of bounds"
1644                );
1645                cols[col_index].push(Cow::from(text.to_owned()));
1646            }
1647            SegType::Single(_) => panic!("add_col_line_indx called on a Single segment"),
1648        }
1649        match &mut self.colors[seg_index] {
1650            SegColor::Columnar(cols) => cols[col_index].push(SegColor::parse_hexcolor(color)),
1651            SegColor::Single(_) => panic!("colors shape mismatch"),
1652        }
1653        self
1654    }
1655
1656    /// Sets the overall alignment of the text box within the terminal.
1657    ///
1658    /// This method controls the horizontal positioning of the entire text box relative to the
1659    /// terminal width. It does not affect the alignment of text within the box segments,
1660    /// which is specified individually when adding segments.
1661    ///
1662    /// # Behaviour with external padding
1663    ///
1664    /// When set to [`BoxAlign::Center`], external left/right padding affects the **width**
1665    /// of the box (more padding → narrower box) but not its position. The box always occupies
1666    /// the center of the terminal regardless of padding values, as long as the terminal is
1667    /// wide enough. See [`padding`](Self::padding) and [`external_padding`](Self::external_padding).
1668    ///
1669    /// # Arguments
1670    ///
1671    /// * `alignment` - The alignment to use: `BoxAlign::Left`, `BoxAlign::Center`, or `BoxAlign::Right`
1672    ///
1673    /// # Returns
1674    ///
1675    /// The builder instance for method chaining
1676    ///
1677    /// # Examples
1678    ///
1679    /// ```
1680    /// use boxy_cli::prelude::*;
1681    ///
1682    /// // center the box — external padding will shrink it but keep it centerd
1683    /// let centered_box = Boxy::builder()
1684    ///     .align(BoxAlign::Center)
1685    ///     .add_segment("centerd in the terminal", "#ffffff", BoxAlign::Left)
1686    ///     .build();
1687    ///
1688    /// // Right-align the box
1689    /// let right_box = Boxy::builder()
1690    ///     .align(BoxAlign::Right)
1691    ///     .add_segment("Aligned to the right", "#ffffff", BoxAlign::Left)
1692    ///     .build();
1693    /// ```
1694    pub fn align(mut self, alignment: BoxAlign) -> Self {
1695        self.align = alignment;
1696        self
1697    }
1698
1699    /// Sets the internal padding between the box border and its text content.
1700    ///
1701    /// Internal padding creates space between the border of the box and the text inside it,
1702    /// providing visual breathing room for the content.
1703    ///
1704    /// # Arguments
1705    ///
1706    /// * `padding` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance specifying the internal padding values
1707    ///
1708    /// # Returns
1709    ///
1710    /// The builder instance for method chaining
1711    ///
1712    /// # Examples
1713    ///
1714    /// ```
1715    /// use boxy_cli::prelude::*;
1716    ///
1717    /// // Set uniform internal padding of 2 spaces on all sides
1718    /// let padded_box = Boxy::builder()
1719    ///     .internal_padding(BoxPad::uniform(2))
1720    ///     .build();
1721    ///
1722    /// // Set different padding for each side (top, left, bottom, right)
1723    /// let custom_pad_box = Boxy::builder()
1724    ///     .internal_padding(BoxPad::from_tldr(1, 3, 1, 3))
1725    ///     .build();
1726    /// ```
1727    pub fn internal_padding(mut self, padding: BoxPad) -> Self {
1728        self.int_padding = padding;
1729        self
1730    }
1731
1732    /// Sets the external padding between the terminal edges and the text box.
1733    ///
1734    /// External padding creates space between the edges of the terminal and the border of the box.
1735    /// This affects the positioning of the box within the terminal.
1736    ///
1737    /// # Arguments
1738    ///
1739    /// * `padding` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance specifying the external padding values
1740    ///
1741    /// # Returns
1742    ///
1743    /// The builder instance for method chaining
1744    ///
1745    /// # Examples
1746    ///
1747    /// ```
1748    /// use boxy_cli::prelude::*;
1749    ///
1750    /// // Add 5 spaces of external padding on all sides
1751    /// let padded_box = Boxy::builder()
1752    ///     .external_padding(BoxPad::uniform(5))
1753    ///     .build();
1754    ///
1755    /// // Add 10 spaces of padding on the left side only
1756    /// let left_padded_box = Boxy::builder()
1757    ///     .external_padding(BoxPad::from_tldr(0, 10, 0, 0))
1758    ///     .build();
1759    /// ```
1760    pub fn external_padding(mut self, padding: BoxPad) -> Self {
1761        self.ext_padding = padding;
1762        self
1763    }
1764
1765    /// Sets both internal and external padding for the text box in a single call.
1766    ///
1767    /// This is a convenience method that combines setting both external padding (between terminal
1768    /// edges and box) and internal padding (between box border and text) in one call.
1769    ///
1770    /// # Arguments
1771    ///
1772    /// * `external` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance for the external padding (between terminal edges and box)
1773    /// * `internal` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance for the internal padding (between box border and text)
1774    ///
1775    /// # Returns
1776    ///
1777    /// The builder instance for method chaining
1778    ///
1779    /// # Examples
1780    ///
1781    /// ```
1782    /// use boxy_cli::prelude::*;
1783    ///
1784    /// // Set both padding types at once
1785    /// let box_with_padding = Boxy::builder()
1786    ///     .padding(
1787    ///         BoxPad::from_tldr(1, 5, 1, 5),  // external padding
1788    ///         BoxPad::uniform(2)              // internal padding
1789    ///     )
1790    ///     .build();
1791    /// ```
1792    pub fn padding(mut self, external: BoxPad, internal: BoxPad) -> Self {
1793        self.ext_padding = external;
1794        self.int_padding = internal;
1795        self
1796    }
1797
1798    /// Sets a fixed width for the box instead of dynamically sizing to the terminal width.
1799    ///
1800    /// By default, the text box automatically adjusts its width based on the terminal size.
1801    /// This method allows you to specify a fixed width instead, which can be useful for
1802    /// creating boxes of consistent size or for controlling the layout of multiple boxes.
1803    ///
1804    /// The `width` value includes the two border characters, so the usable inner text area
1805    /// is `width - 2` columns (minus any internal padding). Pass `0` to return to dynamic
1806    /// terminal-width sizing.
1807    ///
1808    /// # Arguments
1809    ///
1810    /// * `width` - Total box width in terminal columns, including border characters
1811    ///
1812    /// # Returns
1813    ///
1814    /// The builder instance for method chaining
1815    ///
1816    /// # Examples
1817    ///
1818    /// ```
1819    /// use boxy_cli::prelude::*;
1820    ///
1821    /// Boxy::builder()
1822    ///     .width(60) // 60 total: 2 borders + 58 usable
1823    ///     .add_segment("Fixed width box", "#ffffff", BoxAlign::Center)
1824    ///     .build()
1825    ///     .display();
1826    /// ```
1827    ///
1828    /// Setting to 0 restores dynamic sizing:
1829    ///
1830    /// ```
1831    /// use boxy_cli::prelude::*;
1832    ///
1833    /// Boxy::builder()
1834    ///     .width(0) // dynamic — sizes to terminal
1835    ///     .add_segment("Dynamic width", "#ffffff", BoxAlign::Left)
1836    ///     .build();
1837    /// ```
1838    pub fn width(mut self, width: usize) -> Self {
1839        self.fixed_width = width;
1840        self
1841    }
1842
1843    /// Sets a fixed height for the text box by adding whitespace above and below the text.
1844    ///
1845    ///
1846    /// # Note
1847    ///
1848    /// This feature is experimental and may not work as expected in the current version.
1849    /// Setting height to 0 returns to dynamic sizing based on content.
1850    ///
1851    ///
1852    /// This method allows you to specify a fixed height for the box, which can be useful for
1853    /// creating boxes of consistent size or for controlling the layout of multiple boxes.
1854    ///
1855    /// # Arguments
1856    ///
1857    /// * `height` - The desired height in number of lines (including borders)
1858    ///
1859    /// # Returns
1860    ///
1861    /// The builder instance for method chaining
1862    ///
1863    /// # Examples
1864    ///
1865    /// ```
1866    /// use boxy_cli::prelude::*;
1867    ///
1868    /// // Create a box with a fixed height of 20 lines
1869    /// let fixed_height_box = Boxy::builder()
1870    ///     .height(20)
1871    ///     .add_segment("This box has a fixed height", "#ffffff", BoxAlign::Center)
1872    ///     .build();
1873    /// ```
1874    ///
1875    pub fn height(mut self, height: usize) -> Self {
1876        self.fixed_height = height;
1877        self
1878    }
1879
1880    /// Sets the column width ratios for a columnar segment.
1881    ///
1882    /// Ratios are relative — `vec![1, 2, 1]` gives the middle column twice the width
1883    /// of the others. The number of ratios must exactly match the column count the
1884    /// segment was created with. If never called, columns default to equal widths.
1885    ///
1886    /// # Arguments
1887    ///
1888    /// * `seg_index` - Zero-based index of the columnar segment to configure
1889    /// * `ratios` - One ratio value per column; values are relative, not absolute widths
1890    ///
1891    /// # Returns
1892    ///
1893    /// The builder instance for method chaining
1894    ///
1895    /// # Panics
1896    ///
1897    /// Panics if `seg_index` refers to a Single text segment rather than a columnar one,
1898    /// or if `ratios.len()` does not match that segment's column count.
1899    ///
1900    /// # Examples
1901    ///
1902    /// ```
1903    /// use boxy_cli::prelude::*;
1904    ///
1905    /// Boxy::builder()
1906    ///     .add_col_segment(BoxAlign::Left, 3)
1907    ///     .add_col_line("Name", "#aaaaaa", 0)
1908    ///     .add_col_line("Status", "#aaaaaa", 1)
1909    ///     .add_col_line("Notes", "#aaaaaa", 2)
1910    ///     .segment_ratios(0, vec![1, 1, 2]) // Notes column gets twice the space
1911    ///     .build()
1912    ///     .display();
1913    /// ```
1914    pub fn segment_ratios(mut self, seg_index: usize, ratios: Vec<usize>) -> Self {
1915        if seg_index >= self.seg_cols_ratio.len() {
1916            self.seg_cols_ratio.resize(seg_index + 1, Vec::new());
1917        }
1918        self.seg_cols_ratio[seg_index] = ratios;
1919        self
1920    }
1921
1922    /// Adjusts the effective terminal width used for dynamic box sizing.
1923    ///
1924    /// # Note
1925    ///
1926    /// This feature is experimental and may not work as expected in the current version.
1927    /// Setting height to 0 returns to dynamic sizing based on content.
1928    ///
1929    /// When `fixed_width` is not set, the box width defaults to `terminal_width - 20`.
1930    /// This method lets you change that offset. A larger positive value makes the box
1931    /// narrower; a negative value makes it wider than the default. The default offset
1932    /// of `-20` exists to leave a small margin; set to `0` to fill the full terminal width.
1933    ///
1934    /// Has no effect when a fixed width is set via [`width`](Self::width).
1935    ///
1936    /// # Arguments
1937    ///
1938    /// * `offset` - Characters to subtract from the terminal width (negative = wider)
1939    ///
1940    /// # Returns
1941    ///
1942    /// The builder instance for method chaining
1943    ///
1944    /// # Examples
1945    ///
1946    /// ```
1947    /// use boxy_cli::prelude::*;
1948    ///
1949    /// // Fill the full terminal width
1950    /// Boxy::builder()
1951    ///     .set_terminal_width_offset(0)
1952    ///     .add_segment("Full width box", "#ffffff", BoxAlign::Left)
1953    ///     .build();
1954    /// ```
1955    ///
1956    /// # Warning
1957    ///
1958    /// Negative offsets large enough to exceed the terminal width will cause display
1959    /// issues. Prefer [`width`](Self::width) for precise control.
1960    pub fn set_terminal_width_offset(mut self, offset: i32) -> Self {
1961        self.terminal_width_offset = offset;
1962        self
1963    }
1964
1965    /// Consumes the builder and returns a configured [`Boxy`] instance ready to display.
1966    /// (use .display() to output the box to stdout)
1967    ///
1968    /// # Examples
1969    ///
1970    /// ```
1971    /// use boxy_cli::prelude::*;
1972    ///
1973    /// let mut b = Boxy::builder()
1974    ///     .box_type(BoxType::Double)
1975    ///     .color("#00ffff")
1976    ///     .add_segment("Hello, boxy-cli!", "#ffffff", BoxAlign::Center)
1977    ///     .build();
1978    ///
1979    /// b.display();
1980    /// ```
1981    ///
1982    /// Or chain directly into display:
1983    ///
1984    /// ```
1985    /// use boxy_cli::prelude::*;
1986    ///
1987    /// Boxy::builder()
1988    ///     .add_segment("Hello, boxy-cli!", "#ffffff", BoxAlign::Center)
1989    ///     .build()
1990    ///     .display();
1991    /// ```
1992    pub fn build(self) -> Boxy<'a> {
1993        Boxy {
1994            type_enum: self.type_enum,
1995            sect_count: self.data.len(),
1996            data: self.data,
1997            box_col: self.box_col,
1998            colors: self.colors,
1999            int_padding: self.int_padding,
2000            ext_padding: self.ext_padding,
2001            align: self.align,
2002            seg_align: self.seg_align,
2003            fixed_width: self.fixed_width,
2004            fixed_height: self.fixed_height,
2005            seg_cols_count: self.seg_col_count,
2006            seg_cols_ratio: self.seg_cols_ratio,
2007            terminal_width_offset: self.terminal_width_offset,
2008        }
2009    }
2010}