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