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}