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;
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<'a> {
29 type_enum: BoxType,
30 data: Vec<SegType<'a>>,
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<'a> Boxy<'a> {
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<'a> {
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![Cow::from(data_string.to_owned())]));
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(Cow::from(data_string.to_owned())),
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(Cow::from(data_string.to_owned()));
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(Cow::from(data_string.to_owned())),
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: Vec<Vec<usize>> = (0..self.sect_count)
776 .map(|i| match &self.data[i] {
777 SegType::Single(_) => Vec::new(),
778 SegType::Columnar(_) => self.col_widths(&i, &disp_width),
779 })
780 .collect();
781
782 // Preparing the top segment
783 let mut top_seg: String = String::with_capacity(disp_width + self.ext_padding.left + 4);
784 match self.data.first() {
785 None | Some(&SegType::Single(_)) => {
786 write!(
787 top_seg,
788 "{:>width$}",
789 box_pieces.top_left,
790 width = self.ext_padding.left + align_offset
791 )
792 .unwrap();
793 top_seg.push_str(&box_pieces.horizontal.to_string().repeat(disp_width));
794 top_seg.push(box_pieces.top_right);
795 }
796 Some(&SegType::Columnar(_)) => {
797 write!(
798 top_seg,
799 "{:>width$}",
800 box_pieces.top_left,
801 width = self.ext_padding.left + align_offset
802 )
803 .unwrap();
804 let below = self.col_boundaries(&col_widths_segwise[0]);
805 for i in 0..disp_width {
806 match below.contains(&i) {
807 true => {
808 top_seg.push(box_pieces.upper_t);
809 }
810 false => {
811 top_seg.push(box_pieces.horizontal);
812 }
813 };
814 }
815 top_seg.push(box_pieces.top_right);
816 }
817 }
818 // push top segment onto the buffer
819 output_buffer.push(top_seg.color(box_col_truecolor).to_string());
820
821 // Iteratively render all the textbox sections, with appropriate dividers in between
822 for i in 0..self.sect_count {
823 if i > 0 {
824 output_buffer.push(self.render_h_divider(
825 &box_col_truecolor,
826 disp_width,
827 align_offset,
828 &box_pieces,
829 &col_widths_segwise.get(i - 1),
830 &col_widths_segwise.get(i),
831 ));
832 }
833 if let SegType::Single(_) = self.data[i] {
834 // need to move to render, instead of print
835 self.render_segment(
836 i,
837 disp_width,
838 align_offset,
839 &box_pieces,
840 &box_col_truecolor,
841 &mut output_buffer,
842 );
843 } else {
844 // need to move to render, instead of print
845 self.render_cols(
846 i,
847 align_offset,
848 &box_pieces,
849 &box_col_truecolor,
850 &col_widths_segwise[i],
851 &mut output_buffer,
852 );
853 }
854 }
855 // Rendering the bottom segment
856 let mut bot_seg: String = String::with_capacity(disp_width + self.ext_padding.left + 4);
857 match self.data.last() {
858 None | Some(&SegType::Single(_)) => {
859 write!(
860 bot_seg,
861 "{:>width$}",
862 box_pieces.bottom_left,
863 width = self.ext_padding.left + align_offset
864 )
865 .unwrap();
866 bot_seg.push_str(&box_pieces.horizontal.to_string().repeat(disp_width));
867 bot_seg.push(box_pieces.bottom_right);
868 }
869 Some(&SegType::Columnar(_)) => {
870 write!(
871 bot_seg,
872 "{:>width$}",
873 box_pieces.bottom_left,
874 width = self.ext_padding.left + align_offset
875 )
876 .unwrap();
877 let above = self.col_boundaries(
878 col_widths_segwise
879 .last()
880 .expect("failed to get last element"),
881 );
882 for i in 0..disp_width {
883 match above.contains(&i) {
884 true => {
885 bot_seg.push(box_pieces.lower_t);
886 }
887 false => {
888 bot_seg.push(box_pieces.horizontal);
889 }
890 };
891 }
892 bot_seg.push(box_pieces.bottom_right);
893 }
894 }
895 output_buffer.push(bot_seg.color(box_col_truecolor).to_string());
896
897 output_buffer
898 }
899
900 fn render_segment(
901 &mut self,
902 seg_index: usize,
903 disp_width: usize,
904 align_offset: usize,
905 box_pieces: &BoxTemplates,
906 box_col_truecolor: &Color,
907 output_buffer: &mut Vec<String>,
908 ) {
909 let lines = match &self.data[seg_index] {
910 SegType::Single(lines) => lines,
911 SegType::Columnar(_) => return,
912 };
913
914 // Generating new External Pad based on alignment offset
915 let ext_offset = BoxPad {
916 top: self.ext_padding.top,
917 left: self.ext_padding.left + align_offset,
918 right: self.ext_padding.right,
919 down: self.ext_padding.down,
920 };
921
922 for i in 0..lines.len() {
923 // obtaining text colour truevalues
924 let text_col_truecolor = match &self.colors[seg_index] {
925 SegColor::Single(cols) => cols[i],
926 SegColor::Columnar(_) => Color::White, // shouldn't happen in display_segment
927 };
928 // Processing data
929 let processed_data = lines[i].trim().to_owned() + " ";
930
931 let liner: Vec<String> =
932 text_wrap_vec_fast(&processed_data, disp_width, &self.int_padding)
933 .into_iter()
934 .map(|s| s.trim_end().to_owned())
935 .collect();
936
937 // Actually printing shiet
938
939 // Iterative printing. Migrated from recursive to prevent stack overflows with larger text bodies and reduce complexity,
940 // also to improve code efficiency
941 iter_line_rndr(
942 &liner,
943 box_pieces,
944 (box_col_truecolor, &text_col_truecolor),
945 &disp_width,
946 (&ext_offset, &self.int_padding),
947 &self.seg_align[seg_index],
948 output_buffer,
949 );
950
951 // printing an empty line between consecutive non-terminal text line
952 if i < lines.len() - 1 {
953 output_buffer.push(format!(
954 "{1:>width$}{}{1}",
955 " ".repeat(disp_width),
956 box_pieces.vertical.to_string().color(*box_col_truecolor),
957 width = self.ext_padding.left + align_offset
958 ));
959 }
960 }
961 }
962
963 fn render_h_divider(
964 &self,
965 box_col_truecolor: &Color,
966 disp_width: usize,
967 align_offset: usize,
968 box_pieces: &BoxTemplates,
969 prev_seg: &Option<&Vec<usize>>,
970 next_seg: &Option<&Vec<usize>>,
971 ) -> String {
972 let mut div: String = String::with_capacity(disp_width + self.ext_padding.left + 4);
973
974 write!(
975 div,
976 "{:>width$}",
977 box_pieces.left_t.to_string(),
978 width = self.ext_padding.left + align_offset
979 )
980 .unwrap();
981 let empty = Vec::new();
982 let above = self.col_boundaries(prev_seg.unwrap_or(&empty));
983 let below = self.col_boundaries(next_seg.unwrap_or(&empty));
984 for i in 0..disp_width {
985 let ch = match (above.contains(&i), below.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 let graphemes: Vec<&str> = data.graphemes(true).collect();
1122 let total = graphemes.len();
1123 let mut start: usize = 0;
1124
1125 while start < total {
1126 let mut current_cols: usize = 0;
1127 let mut end = start;
1128 let mut last_space_g: Option<usize> = None;
1129
1130 while end < total {
1131 let w = UnicodeWidthStr::width(graphemes[end]);
1132 if current_cols + w > max_cols {
1133 break;
1134 }
1135 if graphemes[end] == " " {
1136 last_space_g = Some(end);
1137 }
1138 current_cols += w;
1139 end += 1;
1140 }
1141 let break_at = if end < total {
1142 last_space_g.unwrap_or(end)
1143 } else {
1144 end
1145 };
1146
1147 liner.push(graphemes[start..break_at].concat());
1148
1149 start = if break_at < total && graphemes[break_at] == " " {
1150 break_at + 1
1151 } else {
1152 break_at
1153 };
1154 }
1155 liner
1156}
1157
1158#[doc(hidden)]
1159fn iter_line_rndr(
1160 liner: &[String],
1161 box_pieces: &BoxTemplates,
1162 context_colors: (&Color, &Color),
1163 disp_width: &usize,
1164 padding: (&BoxPad, &BoxPad),
1165 align: &BoxAlign,
1166 output_buffer: &mut Vec<String>,
1167) {
1168 let (box_col, text_col): (&Color, &Color) = context_colors;
1169 let (ext_padding, int_padding) = padding;
1170 let printable_area = disp_width - int_padding.lr(); // IDK why this works, but it does
1171 let vertical = box_pieces.vertical.to_string().color(*box_col);
1172 match align {
1173 BoxAlign::Left => {
1174 for i in liner.iter() {
1175 let col_width = UnicodeWidthStr::width(i.as_str());
1176 let fill = printable_area
1177 .saturating_sub(col_width)
1178 .saturating_sub(2 * ((int_padding.right == 0) as usize)); // subbing 2 for dynamic sizing w/o internal padding -> bars on each end
1179 let mut currline = String::new();
1180 write!(currline, "{:>width$}", vertical, width = ext_padding.left).unwrap();
1181 write!(currline, "{:<pad$}", " ", pad = int_padding.left).unwrap();
1182 write!(currline, "{}", i.color(*text_col)).unwrap();
1183 write!(currline, "{:<fill$}", " ", fill = fill).unwrap();
1184 write!(currline, "{:<pad$}", " ", pad = int_padding.right).unwrap();
1185 write!(currline, "{}", vertical).unwrap();
1186 output_buffer.push(currline);
1187 }
1188 }
1189 BoxAlign::Center => {
1190 for i in liner.iter() {
1191 let text = i.trim_end();
1192 // display_width not .len(): "日".len()==3 but it only takes 2 columns
1193 let remaining = printable_area.saturating_sub(UnicodeWidthStr::width(text));
1194 let mut currline = String::new();
1195 write!(currline, "{:>width$}", vertical, width = ext_padding.left).unwrap();
1196 write!(
1197 currline,
1198 "{:<pad$}",
1199 " ",
1200 pad = int_padding.left + remaining / 2
1201 )
1202 .unwrap();
1203 write!(currline, "{}", text.color(*text_col)).unwrap();
1204 write!(
1205 currline,
1206 "{:<pad$}",
1207 " ",
1208 pad = int_padding.right + remaining - remaining / 2
1209 )
1210 .unwrap();
1211 write!(currline, "{}", vertical).unwrap();
1212 output_buffer.push(currline);
1213 }
1214 }
1215 BoxAlign::Right => {
1216 for i in liner.iter() {
1217 let col_width = UnicodeWidthStr::width(i.as_str());
1218 let fill = printable_area
1219 .saturating_sub(col_width)
1220 .saturating_sub(2 * ((int_padding.right == 0) as usize)); // subbing 2 for dynamic sizing w/o internal padding -> bars on each end
1221 let mut currline = String::new();
1222 write!(currline, "{:>width$}", vertical, width = ext_padding.left).unwrap();
1223 write!(currline, "{:<pad$}", " ", pad = int_padding.left).unwrap();
1224 write!(currline, "{:<fill$}", " ", fill = fill).unwrap();
1225 write!(currline, "{}", i.color(*text_col)).unwrap();
1226 write!(currline, "{:<pad$}", " ", pad = int_padding.right).unwrap();
1227 write!(currline, "{}", vertical).unwrap();
1228 output_buffer.push(currline);
1229 }
1230 }
1231 }
1232}
1233
1234// returns the box template for the given enum
1235#[doc(hidden)]
1236fn map_box_type(boxtype: &BoxType) -> BoxTemplates {
1237 match boxtype {
1238 BoxType::Classic => CLASSIC_TEMPLATE,
1239 BoxType::Single => SINGLE_TEMPLATE,
1240 BoxType::DoubleHorizontal => DOUB_H_TEMPLATE,
1241 BoxType::DoubleVertical => DOUB_V_TEMPLATE,
1242 BoxType::Double => DOUBLE_TEMPLATE,
1243 BoxType::Bold => BOLD_TEMPLATE,
1244 BoxType::Rounded => ROUNDED_TEMPLATE,
1245 BoxType::BoldCorners => BOLD_CORNERS_TEMPLATE,
1246 BoxType::Empty => EMPTY_TEMPLATE,
1247 }
1248}
1249
1250#[doc(hidden)]
1251fn align_offset(
1252 disp_width: &usize,
1253 term_size: &usize,
1254 align: &BoxAlign,
1255 padding: &BoxPad,
1256) -> usize {
1257 match *align {
1258 BoxAlign::Left => 0,
1259 BoxAlign::Center => (term_size - disp_width) / 2 - padding.left,
1260 BoxAlign::Right => term_size - (disp_width + 2 * padding.right + padding.left),
1261 }
1262}
1263
1264#[cfg(test)]
1265#[doc(hidden)]
1266pub(crate) fn display_width(s: &str) -> usize {
1267 UnicodeWidthStr::width(s)
1268}
1269
1270// Macro type resolution functions for boxy!
1271
1272// These helpers are public so the macro can access them across crate boundaries via $crate::boxer::...
1273// They are hidden from docs and not intended for direct user consumption.
1274#[doc(hidden)]
1275#[allow(dead_code)]
1276pub fn resolve_col(dat: String) -> String {
1277 dat
1278}
1279// Macro type-resolution function
1280#[doc(hidden)]
1281#[allow(dead_code)]
1282pub fn resolve_pad(dat: String) -> BoxPad {
1283 BoxPad::uniform(dat.parse::<usize>().unwrap_or(0usize))
1284}
1285// Macro type-resolution function
1286#[doc(hidden)]
1287#[allow(dead_code)]
1288pub fn resolve_align(dat: String) -> BoxAlign {
1289 match &*dat {
1290 "center" => BoxAlign::Center,
1291 "right" => BoxAlign::Right,
1292 "left" => BoxAlign::Left,
1293 _ => BoxAlign::Left,
1294 }
1295}
1296// Macro type-resolution function
1297#[doc(hidden)]
1298#[allow(dead_code)]
1299pub fn resolve_type(dat: String) -> BoxType {
1300 match &*dat {
1301 "classic" | "c" => BoxType::Classic,
1302 "single" | "s" => BoxType::Single,
1303 "double_horizontal" | "dh" => BoxType::DoubleHorizontal,
1304 "double_vertical" | "dv" => BoxType::DoubleVertical,
1305 "double" | "d" => BoxType::Double,
1306 "bold" | "b" => BoxType::Bold,
1307 "rounded" | "r" => BoxType::Rounded,
1308 "bold_corners" | "bc" => BoxType::BoldCorners,
1309 "empty" | "e" => BoxType::Empty,
1310 _ => BoxType::Single,
1311 }
1312}
1313// Macro type-resolution function
1314#[doc(hidden)]
1315#[allow(dead_code)]
1316pub fn resolve_segments(dat: String) -> usize {
1317 dat.parse().expect("failed to parse total segment number")
1318}
1319
1320// Builder
1321/// The BoxyBuilder struct implements a fluent builder pattern for creating `Boxy` instances.
1322///
1323/// This builder provides a more expressive and readable way to create and configure text boxes.
1324/// Each method returns the builder instance itself, allowing method calls to be chained together.
1325/// When the configuration is complete, call the `build()` method to create the actual [`Boxy`](./struct.Boxy.html) instance.
1326///
1327/// # Examples
1328///
1329/// ```
1330/// use boxy_cli::prelude::*;
1331///
1332/// // Create and display a text box in a single fluent sequence
1333/// Boxy::builder()
1334/// .box_type(BoxType::Double)
1335/// .color("#00ffff")
1336/// .padding(BoxPad::uniform(1), BoxPad::from_tldr(2, 2, 1, 1))
1337/// .align(BoxAlign::Center)
1338/// .add_segment("Hello, Boxy!", "#ffffff", BoxAlign::Center)
1339/// .add_line("This is a new line.", "#32CD32")
1340/// .add_segment("Another section", "#663399", BoxAlign::Left)
1341/// .width(50)
1342/// .build()
1343/// .display();
1344/// ```
1345#[derive(Debug)]
1346pub struct BoxyBuilder<'a> {
1347 type_enum: BoxType,
1348 data: Vec<SegType<'a>>,
1349 box_col: Color,
1350 colors: Vec<SegColor>,
1351 int_padding: BoxPad,
1352 ext_padding: BoxPad,
1353 align: BoxAlign,
1354 seg_align: Vec<BoxAlign>,
1355 fixed_width: usize,
1356 fixed_height: usize,
1357 seg_cols_ratio: Vec<Vec<usize>>,
1358 terminal_width_offset: i32,
1359 seg_col_count: Vec<usize>,
1360}
1361
1362impl<'a> Default for BoxyBuilder<'a> {
1363 fn default() -> Self {
1364 Self::new()
1365 }
1366}
1367
1368impl<'a> BoxyBuilder<'a> {
1369 /// Creates a new `BoxyBuilder` with default values.
1370 ///
1371 /// This creates a builder with the following default values:
1372 /// - Box type: `BoxType::Single`
1373 /// - Color: empty string (will use white if not set)
1374 /// - Padding: zero on all sides
1375 /// - Alignment: `BoxAlign::Left`
1376 /// - No text segments
1377 ///
1378 /// # Examples
1379 ///
1380 /// ```
1381 /// use boxy_cli::prelude::*;
1382 ///
1383 /// let builder = BoxyBuilder::new();
1384 /// // Configure the builder with various methods
1385 /// let my_box = builder.box_type(BoxType::Double)
1386 /// .color("#00ffff")
1387 /// .build();
1388 /// ```
1389 ///
1390 /// Typically used through the `Boxy::builder()` factory method:
1391 ///
1392 ///
1393 /// ```
1394 /// use boxy_cli::prelude::*;
1395 ///
1396 /// let builder = Boxy::builder(); // Same as BoxyBuilder::new()
1397 /// ```
1398 pub fn new() -> Self {
1399 Self {
1400 type_enum: BoxType::Single,
1401 data: Vec::new(),
1402 box_col: Color::White,
1403 colors: Vec::new(),
1404 int_padding: BoxPad::new(),
1405 ext_padding: BoxPad::new(),
1406 align: BoxAlign::Left,
1407 seg_align: Vec::new(),
1408 fixed_width: 0,
1409 fixed_height: 0,
1410 seg_cols_ratio: Vec::new(),
1411 terminal_width_offset: -20,
1412 seg_col_count: Vec::new(),
1413 }
1414 }
1415
1416 /// Sets the border type for the text box.
1417 ///
1418 /// This determines the visual style of the box borders, including the characters used for
1419 /// corners, edges, and intersections. Different styles can create different visual effects,
1420 /// from simple ASCII-style boxes to double-lined or rounded boxes.
1421 ///
1422 /// # Arguments
1423 ///
1424 /// * `box_type` - The border style from the [`BoxType`](../constructs/enum.BoxType.html) enum
1425 ///
1426 /// # Returns
1427 ///
1428 /// The builder instance for method chaining
1429 ///
1430 /// # Examples
1431 ///
1432 /// ```
1433 /// use boxy_cli::prelude::*;
1434 ///
1435 /// let builder = Boxy::builder()
1436 /// .box_type(BoxType::Double); // Use double-lined borders
1437 ///
1438 /// // Or try other border styles
1439 /// let rounded_box = Boxy::builder()
1440 /// .box_type(BoxType::Rounded)
1441 /// .build();
1442 /// ```
1443 pub fn box_type(mut self, box_type: BoxType) -> Self {
1444 self.type_enum = box_type;
1445 self
1446 }
1447
1448 /// Sets the border color for the text box.
1449 ///
1450 /// This method defines the color of the box borders, including corners, edges, and intersections.
1451 /// The color is specified using a hexadecimal color code (e.g. "#00ffff" for cyan).
1452 ///
1453 /// # Arguments
1454 ///
1455 /// * `box_color` - Hex color code (e.g. `\"#ffffff\"`). Falls back to white with a stderr warning on invalid input
1456 ///
1457 /// # Returns
1458 ///
1459 /// The builder instance for method chaining
1460 ///
1461 /// # Examples
1462 ///
1463 /// ```
1464 /// use boxy_cli::prelude::*;
1465 ///
1466 /// // Create a box with cyan borders
1467 /// let cyan_box = Boxy::builder()
1468 /// .color("#00ffff")
1469 /// .build();
1470 ///
1471 /// // Create a box with red borders
1472 /// let red_box = Boxy::builder()
1473 /// .color("#ff0000")
1474 /// .build();
1475 /// ```
1476 ///
1477 /// # Note
1478 ///
1479 /// The actual appearance depends on terminal support for colors.
1480 pub fn color(mut self, box_color: &str) -> Self {
1481 self.box_col = SegColor::parse_hexcolor(box_color);
1482 self
1483 }
1484
1485 /// Adds a new text segment to the box with specified text, color, and alignment.
1486 ///
1487 /// Each segment represents a distinct section of the text box that will be separated by
1488 /// horizontal dividers. This method is used to add the first or subsequent major
1489 /// segments of content.
1490 ///
1491 /// # Arguments
1492 ///
1493 /// * `text` - The text content for this segment
1494 /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1495 /// * `text_align` - The alignment for this text segment (left, center, right)
1496 ///
1497 /// # Returns
1498 ///
1499 /// The builder instance for method chaining
1500 ///
1501 /// # Examples
1502 ///
1503 /// ```
1504 /// use boxy_cli::prelude::*;
1505 ///
1506 /// let my_box = Boxy::builder()
1507 /// // Add a centered header segment in white
1508 /// .add_segment("Header", "#ffffff", BoxAlign::Center)
1509 /// // Add a left-aligned content segment in green
1510 /// .add_segment("Content goes here", "#00ff00", BoxAlign::Left)
1511 /// .build();
1512 /// ```
1513 pub fn add_segment(mut self, text: &str, color: &str, text_align: BoxAlign) -> Self {
1514 self.data
1515 .push(SegType::Single(vec![Cow::from(text.to_owned())]));
1516 self.colors
1517 .push(SegColor::Single(vec![SegColor::parse_hexcolor(color)]));
1518 self.seg_align.push(text_align);
1519 self.seg_col_count.push(0); // Single segment, no columns
1520 self.seg_cols_ratio.push(vec![1]); // placeholder, mirrors add_text_sgmt
1521 self
1522 }
1523
1524 /// Adds a new columnar segment to the box.
1525 /// Adds a new columnar segment to the box with `column_count` side-by-side columns.
1526 ///
1527 /// Columns start empty and are populated with [`add_col_line`](Self::add_col_line) or
1528 /// [`add_col_line_indx`](Self::add_col_line_indx). All columns default to equal width;
1529 /// use [`segment_ratios`](Self::segment_ratios) to customize.
1530 ///
1531 /// # Arguments
1532 ///
1533 /// * `text_align` - Alignment applied to text within each column
1534 /// * `column_count` - Number of columns (must be at least 1)
1535 ///
1536 /// # Returns
1537 ///
1538 /// The builder instance for method chaining
1539 ///
1540 /// # Panics
1541 ///
1542 /// Panics if `column_count` is 0.
1543 ///
1544 /// # Examples
1545 ///
1546 /// ```
1547 /// use boxy_cli::prelude::*;
1548 ///
1549 /// let my_box = Boxy::builder()
1550 /// .add_col_segment(BoxAlign::Left, 2)
1551 /// .add_col_line("Left column", "#ffffff", 0)
1552 /// .add_col_line("Right column", "#ffffff", 1)
1553 /// .build();
1554 /// ```
1555 pub fn add_col_segment(mut self, text_align: BoxAlign, column_count: usize) -> Self {
1556 assert!(
1557 column_count > 0,
1558 "add_col_segment: column_count must be at least 1"
1559 );
1560 self.data
1561 .push(SegType::Columnar(vec![Vec::new(); column_count]));
1562 self.colors
1563 .push(SegColor::Columnar(vec![Vec::new(); column_count]));
1564 self.seg_align.push(text_align);
1565 self.seg_col_count.push(column_count);
1566 self.seg_cols_ratio.push(vec![1; column_count]); // equal widths by default
1567 self
1568 }
1569
1570 /// Adds a new line of text to the most recently added segment.
1571 ///
1572 /// This method adds a line of text to the last segment that was created.
1573 /// The new line will appear below the existing content in that segment.
1574 /// Unlike `add_segment()`, this does not create a new segment with a divider.
1575 ///
1576 /// # Arguments
1577 ///
1578 /// * `text` - The text content to add as a new line
1579 /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1580 ///
1581 /// # Returns
1582 ///
1583 /// The builder instance for method chaining
1584 ///
1585 /// # Examples
1586 ///
1587 /// ```
1588 /// use boxy_cli::prelude::*;
1589 ///
1590 /// let my_box = Boxy::builder()
1591 /// // Add a header segment
1592 /// .add_segment("Header", "#ffffff", BoxAlign::Center)
1593 /// // Add a subheader as a new line in the same segment
1594 /// .add_line("Subheader text", "#aaaaaa")
1595 /// // Add a different segment with a divider
1596 /// .add_segment("Content section", "#00ff00", BoxAlign::Left)
1597 /// .build();
1598 /// ```
1599 ///
1600 pub fn add_line(mut self, text: &str, color: &str) -> Self {
1601 if let Some(last) = self.data.last_mut() {
1602 match last {
1603 SegType::Single(lines) => lines.push(Cow::from(text.to_owned())),
1604 SegType::Columnar(_) => panic!("add_line called on Columnar segment"),
1605 }
1606 match self
1607 .colors
1608 .last_mut()
1609 .expect("colors out of sync with data")
1610 {
1611 SegColor::Single(cols) => cols.push(SegColor::parse_hexcolor(color)),
1612 SegColor::Columnar(_) => panic!("add_line called on Columnar segment"),
1613 }
1614 } else {
1615 // no segment yet — create one, mirroring add_segment
1616 self.data
1617 .push(SegType::Single(vec![Cow::from(text.to_owned())]));
1618 self.colors
1619 .push(SegColor::Single(vec![SegColor::parse_hexcolor(color)]));
1620 self.seg_align.push(BoxAlign::Left);
1621 self.seg_col_count.push(0);
1622 self.seg_cols_ratio.push(vec![1]);
1623 }
1624 self
1625 }
1626
1627 /// Adds a line of text to a specific column of the most recently added columnar segment.
1628 ///
1629 /// Convenience method — no need to specify the segment index. Mirrors
1630 /// [`add_col_line_indx`](Self::add_col_line_indx) for when you're building top-to-bottom.
1631 ///
1632 /// # Arguments
1633 ///
1634 /// * `text` - The text content to add
1635 /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1636 /// * `col_index` - Zero-based index of the column to add this line into
1637 ///
1638 /// # Returns
1639 ///
1640 /// The builder instance for method chaining
1641 ///
1642 /// # Panics
1643 ///
1644 /// Panics if no segment exists, if the last segment is not columnar, or if `col_index`
1645 /// is out of bounds.
1646 ///
1647 /// # Examples
1648 ///
1649 /// ```
1650 /// use boxy_cli::prelude::*;
1651 ///
1652 /// let my_box = Boxy::builder()
1653 /// .add_col_segment(BoxAlign::Left, 3)
1654 /// .add_col_line("Name", "#aaaaaa", 0)
1655 /// .add_col_line("Status", "#aaaaaa", 1)
1656 /// .add_col_line("Notes", "#aaaaaa", 2)
1657 /// .add_col_line("Lumio V2", "#ffffff", 0)
1658 /// .add_col_line("Shipped", "#32CD32", 1)
1659 /// .add_col_line("Done", "#ffffff", 2)
1660 /// .build();
1661 /// ```
1662 pub fn add_col_line(mut self, text: &str, color: &str, col_index: usize) -> Self {
1663 let seg_index = self.data.len() - 1;
1664 match &mut self.data[seg_index] {
1665 SegType::Columnar(cols) => {
1666 assert!(
1667 col_index < cols.len(),
1668 "add_col_line: col_index out of bounds"
1669 );
1670 cols[col_index].push(Cow::from(text.to_owned()));
1671 }
1672 SegType::Single(_) => panic!("add_col_line called on a Single segment"),
1673 }
1674 match &mut self.colors[seg_index] {
1675 SegColor::Columnar(cols) => cols[col_index].push(SegColor::parse_hexcolor(color)),
1676 SegColor::Single(_) => panic!("colors shape mismatch"),
1677 }
1678 self
1679 }
1680
1681 /// Adds a line of text to a specific column of a specific columnar segment by index.
1682 ///
1683 /// Use this when you need to populate segments out of order or return to an earlier
1684 /// segment. For sequential top-to-bottom building, prefer
1685 /// [`add_col_line`](Self::add_col_line).
1686 ///
1687 /// # Arguments
1688 ///
1689 /// * `text` - The text content to add
1690 /// * `color` - Hex color code (e.g. `\"#ffffff\"`) for the text. Falls back to white with a stderr warning on invalid input
1691 /// * `seg_index` - Zero-based index of the columnar segment
1692 /// * `col_index` - Zero-based index of the column within that segment
1693 ///
1694 /// # Returns
1695 ///
1696 /// The builder instance for method chaining
1697 ///
1698 /// # Panics
1699 ///
1700 /// Panics if `seg_index` is out of bounds, if that segment is not columnar, or if
1701 /// `col_index` is out of bounds for that segment's column count.
1702 ///
1703 /// # Examples
1704 ///
1705 /// ```
1706 /// use boxy_cli::prelude::*;
1707 ///
1708 /// let my_box = Boxy::builder()
1709 /// .add_segment("Header", "#ffffff", BoxAlign::Center)
1710 /// .add_col_segment(BoxAlign::Left, 2)
1711 /// .add_col_line_indx("Left", "#ffffff", 1, 0)
1712 /// .add_col_line_indx("Right", "#ffffff", 1, 1)
1713 /// .build();
1714 /// ```
1715 pub fn add_col_line_indx(
1716 mut self,
1717 text: &str,
1718 color: &str,
1719 seg_index: usize,
1720 col_index: usize,
1721 ) -> Self {
1722 match &mut self.data[seg_index] {
1723 SegType::Columnar(cols) => {
1724 assert!(
1725 col_index < cols.len(),
1726 "add_col_line_indx: col_index out of bounds"
1727 );
1728 cols[col_index].push(Cow::from(text.to_owned()));
1729 }
1730 SegType::Single(_) => panic!("add_col_line_indx called on a Single segment"),
1731 }
1732 match &mut self.colors[seg_index] {
1733 SegColor::Columnar(cols) => cols[col_index].push(SegColor::parse_hexcolor(color)),
1734 SegColor::Single(_) => panic!("colors shape mismatch"),
1735 }
1736 self
1737 }
1738
1739 /// Sets the overall alignment of the text box within the terminal.
1740 ///
1741 /// This method controls the horizontal positioning of the entire text box relative to the
1742 /// terminal width. It does not affect the alignment of text within the box segments,
1743 /// which is specified individually when adding segments.
1744 ///
1745 /// # Behaviour with external padding
1746 ///
1747 /// When set to [`BoxAlign::Center`], external left/right padding affects the **width**
1748 /// of the box (more padding → narrower box) but not its position. The box always occupies
1749 /// the center of the terminal regardless of padding values, as long as the terminal is
1750 /// wide enough. See [`padding`](Self::padding) and [`external_padding`](Self::external_padding).
1751 ///
1752 /// # Arguments
1753 ///
1754 /// * `alignment` - The alignment to use: `BoxAlign::Left`, `BoxAlign::Center`, or `BoxAlign::Right`
1755 ///
1756 /// # Returns
1757 ///
1758 /// The builder instance for method chaining
1759 ///
1760 /// # Examples
1761 ///
1762 /// ```
1763 /// use boxy_cli::prelude::*;
1764 ///
1765 /// // center the box — external padding will shrink it but keep it centerd
1766 /// let centered_box = Boxy::builder()
1767 /// .align(BoxAlign::Center)
1768 /// .add_segment("centerd in the terminal", "#ffffff", BoxAlign::Left)
1769 /// .build();
1770 ///
1771 /// // Right-align the box
1772 /// let right_box = Boxy::builder()
1773 /// .align(BoxAlign::Right)
1774 /// .add_segment("Aligned to the right", "#ffffff", BoxAlign::Left)
1775 /// .build();
1776 /// ```
1777 pub fn align(mut self, alignment: BoxAlign) -> Self {
1778 self.align = alignment;
1779 self
1780 }
1781
1782 /// Sets the internal padding between the box border and its text content.
1783 ///
1784 /// Internal padding creates space between the border of the box and the text inside it,
1785 /// providing visual breathing room for the content.
1786 ///
1787 /// # Arguments
1788 ///
1789 /// * `padding` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance specifying the internal padding values
1790 ///
1791 /// # Returns
1792 ///
1793 /// The builder instance for method chaining
1794 ///
1795 /// # Examples
1796 ///
1797 /// ```
1798 /// use boxy_cli::prelude::*;
1799 ///
1800 /// // Set uniform internal padding of 2 spaces on all sides
1801 /// let padded_box = Boxy::builder()
1802 /// .internal_padding(BoxPad::uniform(2))
1803 /// .build();
1804 ///
1805 /// // Set different padding for each side (top, left, bottom, right)
1806 /// let custom_pad_box = Boxy::builder()
1807 /// .internal_padding(BoxPad::from_tldr(1, 3, 1, 3))
1808 /// .build();
1809 /// ```
1810 pub fn internal_padding(mut self, padding: BoxPad) -> Self {
1811 self.int_padding = padding;
1812 self
1813 }
1814
1815 /// Sets the external padding between the terminal edges and the text box.
1816 ///
1817 /// External padding creates space between the edges of the terminal and the border of the box.
1818 /// This affects the positioning of the box within the terminal.
1819 ///
1820 /// # Arguments
1821 ///
1822 /// * `padding` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance specifying the external padding values
1823 ///
1824 /// # Returns
1825 ///
1826 /// The builder instance for method chaining
1827 ///
1828 /// # Examples
1829 ///
1830 /// ```
1831 /// use boxy_cli::prelude::*;
1832 ///
1833 /// // Add 5 spaces of external padding on all sides
1834 /// let padded_box = Boxy::builder()
1835 /// .external_padding(BoxPad::uniform(5))
1836 /// .build();
1837 ///
1838 /// // Add 10 spaces of padding on the left side only
1839 /// let left_padded_box = Boxy::builder()
1840 /// .external_padding(BoxPad::from_tldr(0, 10, 0, 0))
1841 /// .build();
1842 /// ```
1843 pub fn external_padding(mut self, padding: BoxPad) -> Self {
1844 self.ext_padding = padding;
1845 self
1846 }
1847
1848 /// Sets both internal and external padding for the text box in a single call.
1849 ///
1850 /// This is a convenience method that combines setting both external padding (between terminal
1851 /// edges and box) and internal padding (between box border and text) in one call.
1852 ///
1853 /// # Arguments
1854 ///
1855 /// * `external` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance for the external padding (between terminal edges and box)
1856 /// * `internal` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance for the internal padding (between box border and text)
1857 ///
1858 /// # Returns
1859 ///
1860 /// The builder instance for method chaining
1861 ///
1862 /// # Examples
1863 ///
1864 /// ```
1865 /// use boxy_cli::prelude::*;
1866 ///
1867 /// // Set both padding types at once
1868 /// let box_with_padding = Boxy::builder()
1869 /// .padding(
1870 /// BoxPad::from_tldr(1, 5, 1, 5), // external padding
1871 /// BoxPad::uniform(2) // internal padding
1872 /// )
1873 /// .build();
1874 /// ```
1875 pub fn padding(mut self, external: BoxPad, internal: BoxPad) -> Self {
1876 self.ext_padding = external;
1877 self.int_padding = internal;
1878 self
1879 }
1880
1881 /// Sets a fixed width for the box instead of dynamically sizing to the terminal width.
1882 ///
1883 /// By default, the text box automatically adjusts its width based on the terminal size.
1884 /// This method allows you to specify a fixed width instead, which can be useful for
1885 /// creating boxes of consistent size or for controlling the layout of multiple boxes.
1886 ///
1887 /// The `width` value includes the two border characters, so the usable inner text area
1888 /// is `width - 2` columns (minus any internal padding). Pass `0` to return to dynamic
1889 /// terminal-width sizing.
1890 ///
1891 /// # Arguments
1892 ///
1893 /// * `width` - Total box width in terminal columns, including border characters
1894 ///
1895 /// # Returns
1896 ///
1897 /// The builder instance for method chaining
1898 ///
1899 /// # Examples
1900 ///
1901 /// ```
1902 /// use boxy_cli::prelude::*;
1903 ///
1904 /// Boxy::builder()
1905 /// .width(60) // 60 total: 2 borders + 58 usable
1906 /// .add_segment("Fixed width box", "#ffffff", BoxAlign::Center)
1907 /// .build()
1908 /// .display();
1909 /// ```
1910 ///
1911 /// Setting to 0 restores dynamic sizing:
1912 ///
1913 /// ```
1914 /// use boxy_cli::prelude::*;
1915 ///
1916 /// Boxy::builder()
1917 /// .width(0) // dynamic — sizes to terminal
1918 /// .add_segment("Dynamic width", "#ffffff", BoxAlign::Left)
1919 /// .build();
1920 /// ```
1921 pub fn width(mut self, width: usize) -> Self {
1922 self.fixed_width = width;
1923 self
1924 }
1925
1926 /// Sets a fixed height for the text box by adding whitespace above and below the text.
1927 ///
1928 ///
1929 /// # Note
1930 ///
1931 /// This feature is experimental and may not work as expected in the current version.
1932 /// Setting height to 0 returns to dynamic sizing based on content.
1933 ///
1934 ///
1935 /// This method allows you to specify a fixed height for the box, which can be useful for
1936 /// creating boxes of consistent size or for controlling the layout of multiple boxes.
1937 ///
1938 /// # Arguments
1939 ///
1940 /// * `height` - The desired height in number of lines (including borders)
1941 ///
1942 /// # Returns
1943 ///
1944 /// The builder instance for method chaining
1945 ///
1946 /// # Examples
1947 ///
1948 /// ```
1949 /// use boxy_cli::prelude::*;
1950 ///
1951 /// // Create a box with a fixed height of 20 lines
1952 /// let fixed_height_box = Boxy::builder()
1953 /// .height(20)
1954 /// .add_segment("This box has a fixed height", "#ffffff", BoxAlign::Center)
1955 /// .build();
1956 /// ```
1957 ///
1958 pub fn height(mut self, height: usize) -> Self {
1959 self.fixed_height = height;
1960 self
1961 }
1962
1963 /// Sets the column width ratios for a columnar segment.
1964 ///
1965 /// Ratios are relative — `vec![1, 2, 1]` gives the middle column twice the width
1966 /// of the others. The number of ratios must exactly match the column count the
1967 /// segment was created with. If never called, columns default to equal widths.
1968 ///
1969 /// # Arguments
1970 ///
1971 /// * `seg_index` - Zero-based index of the columnar segment to configure
1972 /// * `ratios` - One ratio value per column; values are relative, not absolute widths
1973 ///
1974 /// # Returns
1975 ///
1976 /// The builder instance for method chaining
1977 ///
1978 /// # Panics
1979 ///
1980 /// Panics if `seg_index` refers to a Single text segment rather than a columnar one,
1981 /// or if `ratios.len()` does not match that segment's column count.
1982 ///
1983 /// # Examples
1984 ///
1985 /// ```
1986 /// use boxy_cli::prelude::*;
1987 ///
1988 /// Boxy::builder()
1989 /// .add_col_segment(BoxAlign::Left, 3)
1990 /// .add_col_line("Name", "#aaaaaa", 0)
1991 /// .add_col_line("Status", "#aaaaaa", 1)
1992 /// .add_col_line("Notes", "#aaaaaa", 2)
1993 /// .segment_ratios(0, vec![1, 1, 2]) // Notes column gets twice the space
1994 /// .build()
1995 /// .display();
1996 /// ```
1997 pub fn segment_ratios(mut self, seg_index: usize, ratios: Vec<usize>) -> Self {
1998 if seg_index >= self.seg_cols_ratio.len() {
1999 self.seg_cols_ratio.resize(seg_index + 1, Vec::new());
2000 }
2001 self.seg_cols_ratio[seg_index] = ratios;
2002 self
2003 }
2004
2005 /// Adjusts the effective terminal width used for dynamic box sizing.
2006 ///
2007 /// # Note
2008 ///
2009 /// This feature is experimental and may not work as expected in the current version.
2010 /// Setting height to 0 returns to dynamic sizing based on content.
2011 ///
2012 /// When `fixed_width` is not set, the box width defaults to `terminal_width - 20`.
2013 /// This method lets you change that offset. A larger positive value makes the box
2014 /// narrower; a negative value makes it wider than the default. The default offset
2015 /// of `-20` exists to leave a small margin; set to `0` to fill the full terminal width.
2016 ///
2017 /// Has no effect when a fixed width is set via [`width`](Self::width).
2018 ///
2019 /// # Arguments
2020 ///
2021 /// * `offset` - Characters to subtract from the terminal width (negative = wider)
2022 ///
2023 /// # Returns
2024 ///
2025 /// The builder instance for method chaining
2026 ///
2027 /// # Examples
2028 ///
2029 /// ```
2030 /// use boxy_cli::prelude::*;
2031 ///
2032 /// // Fill the full terminal width
2033 /// Boxy::builder()
2034 /// .set_terminal_width_offset(0)
2035 /// .add_segment("Full width box", "#ffffff", BoxAlign::Left)
2036 /// .build();
2037 /// ```
2038 ///
2039 /// # Warning
2040 ///
2041 /// Negative offsets large enough to exceed the terminal width will cause display
2042 /// issues. Prefer [`width`](Self::width) for precise control.
2043 pub fn set_terminal_width_offset(mut self, offset: i32) -> Self {
2044 self.terminal_width_offset = offset;
2045 self
2046 }
2047
2048 /// Consumes the builder and returns a configured [`Boxy`] instance ready to display.
2049 /// (use .display() to output the box to stdout)
2050 ///
2051 /// # Examples
2052 ///
2053 /// ```
2054 /// use boxy_cli::prelude::*;
2055 ///
2056 /// let mut b = Boxy::builder()
2057 /// .box_type(BoxType::Double)
2058 /// .color("#00ffff")
2059 /// .add_segment("Hello, boxy-cli!", "#ffffff", BoxAlign::Center)
2060 /// .build();
2061 ///
2062 /// b.display();
2063 /// ```
2064 ///
2065 /// Or chain directly into display:
2066 ///
2067 /// ```
2068 /// use boxy_cli::prelude::*;
2069 ///
2070 /// Boxy::builder()
2071 /// .add_segment("Hello, boxy-cli!", "#ffffff", BoxAlign::Center)
2072 /// .build()
2073 /// .display();
2074 /// ```
2075 pub fn build(self) -> Boxy<'a> {
2076 Boxy {
2077 type_enum: self.type_enum,
2078 sect_count: self.data.len(),
2079 data: self.data,
2080 box_col: self.box_col,
2081 colors: self.colors,
2082 int_padding: self.int_padding,
2083 ext_padding: self.ext_padding,
2084 align: self.align,
2085 seg_align: self.seg_align,
2086 fixed_width: self.fixed_width,
2087 fixed_height: self.fixed_height,
2088 seg_cols_count: self.seg_col_count,
2089 seg_cols_ratio: self.seg_cols_ratio,
2090 terminal_width_offset: self.terminal_width_offset,
2091 }
2092 }
2093}