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}