boxy_cli/boxer.rs
1//! The main crate logic
2
3use std::borrow::Cow;
4use colored::{Color, Colorize};
5use hex_color::HexColor;
6use crate::templates::*;
7use crate::constructs::*;
8
9
10/// The main struct that represents a text box for CLI display.
11///
12/// `Boxy` contains all the configuration and content needed to render a styled text box
13/// in the terminal, including borders, text content, colors, padding, and alignment options.
14///
15/// # Examples
16///
17/// ```
18/// use boxy_cli::prelude::*;
19///
20/// // Create a simple text box
21/// let mut my_box = Boxy::new(BoxType::Double, "#00ffff");
22/// my_box.add_text_sgmt("Hello, World!", "#ffffff", BoxAlign::Center);
23/// my_box.display();
24/// ```
25#[derive(Debug)]
26pub struct Boxy<'a> {
27 pub type_enum: BoxType,
28 pub data : Vec<Vec<Cow<'a, str>>>,
29 pub sect_count: usize,
30 pub box_col : String,
31 pub colors : Vec<Vec<Cow<'a, str>>>,
32 pub int_padding: BoxPad,
33 pub ext_padding: BoxPad,
34 pub align : BoxAlign,
35 pub seg_align: Vec<BoxAlign>,
36 pub fixed_width: usize,
37 pub fixed_height: usize,
38 pub seg_v_div_count: Vec<usize>,
39 pub seg_v_div_ratio: Vec<Vec<usize>>,
40 pub tot_seg: usize,
41 pub terminal_width_offset: i32,
42}
43
44// Default struct values for the textbox
45impl Default for Boxy<'_> {
46 fn default() -> Self {
47 Self {
48 type_enum: BoxType::Single,
49 data : Vec::<Vec<Cow<str>>>::new(),
50 sect_count: 0usize,
51 box_col: "#ffffff".to_string(),
52 colors : Vec::<Vec<Cow<str>>>::new(),
53 int_padding: BoxPad::new(),
54 ext_padding: BoxPad::new(),
55 align : BoxAlign::Left,
56 seg_align: Vec::<BoxAlign>::new(),
57 fixed_width: 0usize,
58 fixed_height: 0usize,
59 seg_v_div_ratio: Vec::<Vec<usize>>::new(),
60 seg_v_div_count: Vec::<usize>::new(),
61 tot_seg: 0usize,
62 terminal_width_offset: -20
63 }
64 }
65}
66
67
68impl<'a> Boxy<'a> {
69 /// Creates a new instance of the `Boxy` struct with the specified border type and color.
70 ///
71 /// # Arguments
72 ///
73 /// * `box_type` - The border style to use from the `BoxType` enum
74 /// * `box_color` - A hex color code (e.g. "#00ffff") for the border color
75 ///
76 /// # Examples
77 ///
78 /// ```
79 /// use boxy_cli::prelude::*;
80 ///
81 /// let mut my_box = Boxy::new(BoxType::Double, "#00ffff");
82 /// ```
83 pub fn new(box_type: BoxType, box_color : &str) -> Self {
84 Boxy {
85 type_enum: box_type,
86 box_col: box_color.to_string(),
87 ..Self::default()
88 }
89 }
90 /// Returns a new `BoxyBuilder` to create a textbox using the builder pattern.
91 ///
92 /// The builder pattern provides a more fluent interface for configuring and creating a `Boxy` instance.
93 ///
94 /// # Examples
95 ///
96 /// ```
97 /// use boxy_cli::prelude::*;
98 ///
99 /// let my_box = Boxy::builder()
100 /// .box_type(BoxType::Double)
101 /// .color("#00ffff")
102 /// .add_segment("Hello, World!", "#ffffff", BoxAlign::Center)
103 /// .build();
104 /// ```
105 pub fn builder() -> BoxyBuilder <'a> {
106 BoxyBuilder::new()
107 }
108
109 /// Adds a new text segment/section to the textbox, separated by a horizontal divider.
110 ///
111 /// Each segment represents a distinct section of the textbox that will be separated by
112 /// horizontal dividers. This method is typically used to add the first or later major
113 /// sections of content.
114 ///
115 /// # Arguments
116 ///
117 /// * `data_string` - The text content for this segment
118 /// * `color` - A hex color code (e.g. "#ffffff") for the text color
119 /// * `text_align` - The alignment (left, center, right) for this text segment
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use boxy_cli::prelude::*;
125 ///
126 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
127 /// my_box.add_text_sgmt("Header section", "#ffffff", BoxAlign::Center);
128 /// my_box.add_text_sgmt("Content section", "#ffffff", BoxAlign::Left);
129 /// ```
130 pub fn add_text_sgmt(&mut self, data_string : &str, color : &str, text_align: BoxAlign) {
131 self.data.push(vec![Cow::from(data_string.to_owned())]);
132 self.colors.push(vec![Cow::from(String::from(color))]);
133 self.seg_align.push(text_align);
134 self.sect_count+=1;
135 }
136
137 /// Adds a new text line to the segment with a specific index.
138 ///
139 /// This method allows adding additional lines of text to an existing segment by specifying
140 /// the segment's index. The new line will appear below the existing content in that segment.
141 ///
142 /// # Arguments
143 ///
144 /// * `data_string` - The text content to add
145 /// * `color` - A hex color code (e.g. "#ffffff") for the text color
146 /// * `seg_index` - The index of the segment to add this line to (0-based)
147 ///
148 /// # Examples
149 ///
150 /// ```
151 /// use boxy_cli::prelude::*;
152 ///
153 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
154 /// my_box.add_text_sgmt("First segment", "#ffffff", BoxAlign::Left);
155 /// my_box.add_text_sgmt("Second segment", "#ffffff", BoxAlign::Left);
156 ///
157 /// // Add a line to the first segment (index 0)
158 /// my_box.add_text_line_indx("Additional line in first segment", "#32CD32", 0);
159 /// ```
160 ///
161 /// # Panics
162 ///
163 /// Panics if `seg_index` is out of bounds.
164 pub fn add_text_line_indx(&mut self, data_string : &str, color: &str, seg_index : usize) {
165 self.data[seg_index].push(Cow::from(data_string.to_owned()));
166 self.colors[seg_index].push(Cow::from(String::from(color)));
167 }
168
169 /// Adds a new text line to the most recently added segment.
170 ///
171 /// This is a convenience method that adds a line of text to the last segment that was
172 /// created, eliminating the need to specify the segment index.
173 ///
174 /// # Arguments
175 ///
176 /// * `data_string` - The text content to add
177 /// * `color` - A hex color code (e.g. "#ffffff") for the text color
178 ///
179 /// # Examples
180 ///
181 /// ```
182 /// use boxy_cli::prelude::*;
183 ///
184 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
185 /// my_box.add_text_sgmt("Header", "#ffffff", BoxAlign::Center);
186 /// my_box.add_text_line("Additional details below the header", "#32CD32");
187 /// ```
188 ///
189 /// # Panics
190 ///
191 /// Panics if no segments have been added yet.
192 pub fn add_text_line(&mut self, data_string : &str, color : &str) {
193 self.data[self.sect_count-1].push(Cow::from(data_string.to_owned()));
194 self.colors[self.sect_count-1].push(Cow::from(String::from(color)));
195 }
196
197 /// Sets the overall alignment of the textbox within the terminal.
198 ///
199 /// This controls the horizontal positioning of the entire textbox relative to the terminal width.
200 /// It does not affect the alignment of text within the box segments.
201 ///
202 /// # Arguments
203 ///
204 /// * `align` - The alignment to use: `BoxAlign::Left`, `BoxAlign::Center`, or `BoxAlign::Right`
205 ///
206 /// # Examples
207 ///
208 /// ```
209 /// use boxy_cli::prelude::*;
210 ///
211 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
212 /// my_box.set_align(BoxAlign::Center); // Center the box in the terminal
213 /// ```
214 pub fn set_align(&mut self, align: BoxAlign) {
215 self.align = align;
216 }
217
218 /// Sets the internal padding between the textbox border and its text content.
219 ///
220 /// Internal padding creates space between the border of the box and the text inside it.
221 ///
222 /// # Arguments
223 ///
224 /// * `int_padding` - A `BoxPad` instance specifying the padding values
225 ///
226 /// # Examples
227 ///
228 /// ```
229 /// use boxy_cli::prelude::*;
230 ///
231 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
232 ///
233 /// // Set uniform padding of 2 spaces on all sides
234 /// my_box.set_int_padding(BoxPad::uniform(2));
235 ///
236 /// // Or set different padding for each side (top, left, down, right)
237 /// my_box.set_int_padding(BoxPad::from_tldr(1, 3, 1, 3));
238 /// ```
239 pub fn set_int_padding(&mut self, int_padding : BoxPad) {
240 self.int_padding = int_padding;
241 }
242 /// Sets the external padding between the terminal edges and the textbox.
243 ///
244 /// External padding creates space between the edges of the terminal and the border of the box.
245 /// This affects the positioning of the box within the terminal.
246 ///
247 /// # Arguments
248 ///
249 /// * `ext_padding` - A `BoxPad` instance specifying the padding values
250 ///
251 /// # Examples
252 ///
253 /// ```
254 /// use boxy_cli::prelude::*;
255 ///
256 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
257 ///
258 /// // Add 5 spaces of padding on all sides
259 /// my_box.set_ext_padding(BoxPad::uniform(5));
260 ///
261 /// // Or set different padding for each side (top, left, down, right)
262 /// my_box.set_ext_padding(BoxPad::from_tldr(0, 10, 0, 0));
263 /// ```
264 pub fn set_ext_padding(&mut self, ext_padding : BoxPad) {
265 self.ext_padding = ext_padding;
266 }
267 /// Sets both internal and external padding for the textbox in a single call.
268 ///
269 /// This is a convenience method that combines `set_int_padding` and `set_ext_padding`.
270 ///
271 /// # Arguments
272 ///
273 /// * `ext_padding` - A `BoxPad` instance for the external padding (between terminal edges and box)
274 /// * `int_padding` - A `BoxPad` instance for the internal padding (between box border and text)
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// use boxy_cli::prelude::*;
280 ///
281 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
282 ///
283 /// // Set both internal and external padding
284 /// my_box.set_padding(
285 /// BoxPad::from_tldr(1, 5, 1, 5), // external padding
286 /// BoxPad::uniform(2) // internal padding
287 /// );
288 /// ```
289 pub fn set_padding(&mut self, ext_padding : BoxPad, int_padding : BoxPad) {
290 self.int_padding = int_padding;
291 self.ext_padding = ext_padding;
292 }
293
294 /// Sets a fixed width for the textbox instead of dynamically sizing it to the terminal width.
295 ///
296 /// By default, the textbox automatically adjusts its width based on the terminal size.
297 /// This method allows you to specify a fixed width instead.
298 ///
299 /// # Arguments
300 ///
301 /// * `width` - The desired width in characters (including borders)
302 ///
303 /// # Examples
304 ///
305 /// ```
306 /// use boxy_cli::prelude::*;
307 ///
308 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
309 /// my_box.set_width(50); // Fix the box width to 50 characters
310 /// ```
311 ///
312 /// # Note
313 ///
314 /// Setting width to 0 returns to dynamic sizing based on terminal width.
315 pub fn set_width(&mut self, width : usize) {
316 self.fixed_width = width;
317 }
318
319 /// Sets a fixed height for the textbox by adding whitespace above and below the text.
320 ///
321 /// # Arguments
322 ///
323 /// * `height` - The desired height in characters (including borders)
324 ///
325 /// # Examples
326 ///
327 /// ```
328 /// use boxy_cli::prelude::*;
329 ///
330 /// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
331 /// my_box.set_height(20); // Set box height to 20 lines
332 /// ```
333 ///
334 /// # Note
335 ///
336 /// This feature is experimental and may not work as expected in the current version.
337 /// Setting height to 0 returns to dynamic sizing based on content.
338 pub fn set_height(&mut self, height : usize) {
339 self.fixed_height = height;
340 }
341
342 /// Sets the size-ratio between segments when using vertical divisions
343 ///
344 /// This feature is still experimental and not yet implemented fully, and hence may not work in the current version of the crate.
345 pub fn set_segment_ratios(&mut self, seg_index: usize, ratios: Vec<usize>) {
346 if seg_index >= self.seg_v_div_ratio.len() {
347 self.seg_v_div_ratio.resize(seg_index + 1, Vec::new());
348 }
349 self.seg_v_div_ratio[seg_index] = ratios;
350 }
351
352 /// Renders and displays the textbox in the terminal.
353 ///
354 /// This method performs all the necessary calculations to render the textbox with the
355 /// configured settings, including border style, colors, padding, and text content.
356 /// It then prints the textbox to the standard output.
357 ///
358 /// # Examples
359 ///
360 /// ```
361 /// use boxy_cli::prelude::*;
362 ///
363 /// let mut my_box = Boxy::new(BoxType::Double, "#00ffff");
364 /// my_box.add_text_sgmt("Hello, World!", "#ffffff", BoxAlign::Center);
365 /// my_box.display(); // Renders the box to the terminal
366 /// ```
367 ///
368 /// # Note
369 ///
370 /// The appearance may vary depending on terminal support for colors and Unicode characters.
371 pub fn display(&mut self) {
372
373 // Initialising Display Variables
374
375 let term_size = termsize::get().unwrap_or_else( ||
376 {
377 eprintln!("Failed to get terminal size, assuming default width of 80");
378 termsize::Size { rows: 10, cols: 80 }
379 }
380 ).cols as usize;
381
382 let disp_width = if self.fixed_width !=0 {
383 self.fixed_width - 2
384 } else {
385 term_size - self.ext_padding.lr() - 2
386 };
387
388 let box_col_truecolor = match HexColor::parse(&self.box_col) {
389 Ok(color) => Color::TrueColor { r: color.r, g: color.g, b: color.b },
390 Err(e) => {
391 eprintln!("Error parsing box color '{}': {}", &self.box_col, e);
392 Color::White // Default color
393 }
394 };
395 let box_pieces = map_box_type(&self.type_enum);
396 let horiz =box_pieces.horizontal.to_string().color(box_col_truecolor);
397
398 let align_offset = align_offset(&disp_width, &term_size, &self.align, &self.ext_padding);
399
400 // Printing the top segment
401 print!("{:>width$}", box_pieces.top_left.to_string().color(box_col_truecolor), width=self.ext_padding.left+1+align_offset);
402 for _ in 0..disp_width {
403 print!("{}", horiz);
404 }
405 println!("{}", box_pieces.top_right.to_string().color(box_col_truecolor));
406
407 // Iteratively print all the textbox sections, with appropriate dividers in between
408 for i in 0..self.sect_count {
409 if i > 0 {
410 self.print_h_divider(&self.box_col.clone(), &disp_width, &align_offset);
411 }
412 self.display_segment(i, &disp_width, &align_offset);
413 }
414
415 // Printing the bottom segment
416 print!("{:>width$}", box_pieces.bottom_left.to_string().color(box_col_truecolor), width=self.ext_padding.left+1+align_offset);
417 for _ in 0..disp_width {
418 print!("{}", horiz);
419 }
420 println!("{}", box_pieces.bottom_right.to_string().color(box_col_truecolor));
421
422 }
423
424 // Displaying each segment body
425 fn display_segment(&mut self, seg_index: usize, disp_width: &usize, align_offset: &usize) {
426
427 // TODO: Add functionality to create segments while displaying the textbox i.e. columns
428 let box_col_truecolor = match HexColor::parse(&self.box_col) {
429 Ok(color) => Color::TrueColor { r: color.r, g: color.g, b: color.b },
430 Err(e) => {
431 eprintln!("Error parsing box color '{}': {}", &self.box_col, e);
432 Color::White // Default color
433 }
434 };
435
436 // Loop for all text lines
437 for i in 0..self.data[seg_index].len() {
438 // obtaining text colour truevalues
439 let text_col_truecolor = match HexColor::parse(&self.colors[seg_index][i]) {
440 Ok(color) => Color::TrueColor { r: color.r, g: color.g, b: color.b },
441 Err(e) => {
442 eprintln!("Error parsing text color '{}': {}", &self.colors[seg_index][i], e);
443 Color::White // Default color
444 }
445 };
446 // Processing data
447 let processed_data = self.data[seg_index][i].trim().to_owned() + " ";
448
449 let mut ws_indices = processed_data.as_bytes().iter().enumerate().filter(|(_, b)| **b == b' ').map(|(i, _)| i).collect::<Vec<usize>>();
450
451 let liner: Vec<String> = text_wrap_vec(&processed_data, &mut ws_indices, &disp_width.clone(), &self.int_padding);
452
453 // Generating new External Pad based on alignment offset
454 let ext_offset = BoxPad {
455 top: self.ext_padding.top,
456 left: self.ext_padding.left + align_offset,
457 right: self.ext_padding.right,
458 down: self.ext_padding.down,
459 };
460
461 // Actually printing shiet
462
463 // Iterative printing. Migrated from recursive to prevent stack overflows with larger text bodies and reduce complexity, also to improve code efficiency
464 iter_line_prnt(&liner, map_box_type(&self.type_enum), &box_col_truecolor, &text_col_truecolor, (disp_width, &(self.fixed_width != 0)), (&ext_offset, &self.int_padding), &self.seg_align[seg_index]);
465
466 // printing an empty line between consecutive non-terminal text line
467 if i < self.data[seg_index].len() - 1 {
468 println!("{1:>width$}{}{1}", " ".repeat(*disp_width),
469 map_box_type(&self.type_enum)
470 .vertical.to_string()
471 .color(box_col_truecolor),
472 width=self.ext_padding.left+1+align_offset);
473 }
474 }
475 // Recursive Printing of text -> now depreciated
476 // recur_whitespace_printing(&processed_data, &mut ws_indices, &self.type_enum, &terminal_size, 0usize, &col_truevals, &self.ext_padding, &self.int_padding, &self.align);
477 }
478
479 // Printing the horizontal divider.
480 fn print_h_divider(&mut self, boxcol_hex: &str, disp_width: &usize, align_offset: &usize) {
481 let box_pieces = map_box_type(&self.type_enum);
482 let box_col_truecolor = match HexColor::parse(boxcol_hex) {
483 Ok(color) => Color::TrueColor { r: color.r, g: color.g, b: color.b },
484 Err(e) => {
485 eprintln!("Error parsing divider color '{}': {}", boxcol_hex, e);
486 Color::White // Default color
487 }
488 };
489 let horiz = box_pieces.horizontal.to_string().color(box_col_truecolor);
490 print!("{:>width$}", box_pieces.left_t.to_string().color(box_col_truecolor), width=self.ext_padding.left+1+align_offset);
491 for _ in 0..*disp_width {
492 print!("{}", horiz);
493 }
494 println!("{}", box_pieces.right_t.to_string().color(box_col_truecolor));
495 }
496}
497
498// Function to find the next-most-fitting string slice for the give terminal size
499
500fn nearest_whitespace(map: &mut Vec<usize>, printable_length: &usize, start_index: usize) -> usize {
501 let mut next_ws = 0;
502 for i in map {
503 if *i > start_index && *i-start_index <= *printable_length {
504 next_ws = *i;
505 }
506 }
507 // force line break if no appropriate whitespace found
508 if next_ws == 0 {
509 next_ws = start_index + printable_length;
510 }
511 next_ws
512}
513
514// Recursively printing the next text segment into the textbox
515
516// Went with recursive as that is just more modular, and I can just reuse this code for printing horizontal and vertical segments.
517
518fn text_wrap_vec(data:&str, map: &mut Vec<usize>, disp_width: &usize, int_padding: &BoxPad) -> Vec<String> {
519 let mut liner: Vec<String> = Vec::new();
520 let mut start_index = 0;
521
522 while start_index < data.len() {
523 let next_ws = nearest_whitespace(map, &(disp_width - int_padding.lr() - 2), start_index);
524 liner.push(data[start_index..next_ws].to_string());
525 if next_ws >= data.len()-1 {break;}
526 start_index = next_ws+1;
527 }
528 liner
529
530 // Legacy recursive code. Depreciated to increase efficiency for larger use cases
531 /*
532 let next_ws = nearest_whitespace(map, &(term_size - 2*(int_padding + ext_padding)), start_index);
533 line_vec.push(String::from(&data[start_index..next_ws]));
534 if next_ws < (data.len()-1) {
535 text_wrap_vec(data, map, term_size, next_ws+1, ext_padding, int_padding, line_vec);
536 }
537 */
538}
539
540
541fn iter_line_prnt(liner : &[String], box_pieces:BoxTemplates, box_col: &Color, text_col: &Color, disp_params: (&usize, &bool), padding: (&BoxPad, &BoxPad), align: &BoxAlign) {
542 let (ext_padding, int_padding) = padding;
543 let (disp_width, fixed_size) = disp_params;
544 let printable_area = disp_width - int_padding.lr() + 2*((int_padding.left!=0) as usize)*(!*fixed_size as usize); // IDK why this works, but it does
545 let vertical = box_pieces.vertical.to_string().color(*box_col);
546 match align {
547 BoxAlign::Left => {
548 for i in liner.iter() {
549 print!("{:>width$}", vertical, width=ext_padding.left+1);
550 print!("{:<pad$}", " ", pad=int_padding.left);
551 print!("{:<width$}", i.color(*text_col), width=printable_area-(2*(!*fixed_size as usize))); // subtract 2 for the bars if on dynamic sizing
552 print!("{:<pad$}", " ", pad=int_padding.right);
553 println!("{}", vertical);
554 }
555 },
556 BoxAlign::Center => {
557 for i in liner.iter() {
558 print!("{:>width$}", vertical, width=ext_padding.left+1);
559 print!("{:<pad$}", " ", pad=int_padding.left+((printable_area-i.len())/2));
560 print!("{}", i.color(*text_col));
561 print!("{:<pad$}", " ", pad=int_padding.right+(printable_area-i.len())-((printable_area-i.len())/2));
562 println!("{}", vertical);
563 }
564 },
565 BoxAlign::Right => {
566 for i in liner.iter() {
567 print!("{:>width$}", vertical, width=ext_padding.left+1);
568 print!("{:<pad$}", " ", pad=int_padding.left);
569 print!("{:>width$}", i.color(*text_col), width=printable_area-(2*(!*fixed_size as usize))); // subtract 2 for the bars if on dynamic sizing
570 print!("{:<pad$}", " ", pad=int_padding.right);
571 println!("{}", vertical);
572 }
573 }
574 }
575}
576
577// returns the box template for the given enum
578fn map_box_type (boxtype : &BoxType) -> BoxTemplates{
579 match boxtype {
580 BoxType::Classic => CLASSIC_TEMPLATE,
581 BoxType::Single => SINGLE_TEMPLATE,
582 BoxType::DoubleHorizontal => DOUB_H_TEMPLATE,
583 BoxType::DoubleVertical => DOUB_V_TEMPLATE,
584 BoxType::Double => DOUBLE_TEMPLATE,
585 BoxType::Bold => BOLD_TEMPLATE,
586 BoxType::Rounded => ROUNDED_TEMPLATE,
587 BoxType::BoldCorners => BOLD_CORNERS_TEMPLATE,
588 }
589}
590
591fn align_offset(disp_width: &usize, term_size: &usize, align: &BoxAlign, padding: &BoxPad) -> usize {
592 match *align {
593 BoxAlign::Left => {
594 0
595 },
596 BoxAlign::Center => {
597 (term_size-disp_width)/2 - padding.left
598 },
599 BoxAlign::Right => {
600 term_size-(disp_width + 2*padding.right + padding.left)
601 }
602 }
603}
604
605// Macro type resolution fucntions for boxy!
606
607
608/// Macro type-resolution function
609pub fn resolve_col(dat : String) -> String {
610 dat
611}
612/// Macro type-resolution function
613pub fn resolve_pad(dat : String) -> BoxPad {
614 BoxPad::uniform(dat.parse::<usize>().unwrap_or(0usize))
615}
616/// Macro type-resolution function
617pub fn resolve_align(dat : String) -> BoxAlign {
618 match &*dat {
619 "center" => BoxAlign::Center,
620 "right" => BoxAlign::Right,
621 "left" => BoxAlign::Left,
622 _ => BoxAlign::Left,
623 }
624}
625/// Macro type-resolution function
626pub fn resolve_type(dat : String) -> BoxType{
627 match &*dat {
628 "classic" => BoxType::Classic,
629 "single" => BoxType::Single,
630 "double_horizontal" => BoxType::DoubleHorizontal,
631 "double_vertical" => BoxType::DoubleVertical,
632 "double" => BoxType::Double,
633 "bold" => BoxType::Bold,
634 "rounded" => BoxType::Rounded,
635 "bold_corners" => BoxType::BoldCorners,
636 _ => BoxType::Single,
637 }
638}
639/// Macro type-resolution function
640pub fn resolve_segments(dat : String) -> usize {
641 dat.parse().expect("failed to parse total segment number")
642}
643
644// Builder
645/// The BoxyBuilder struct implements a fluent builder pattern for creating `Boxy` instances.
646///
647/// This builder provides a more expressive and readable way to create and configure text boxes.
648/// Each method returns the builder instance itself, allowing method calls to be chained together.
649/// When the configuration is complete, call the `build()` method to create the actual [`Boxy`](./struct.Boxy.html) instance.
650///
651/// # Examples
652///
653/// ```
654/// use boxy_cli::prelude::*;
655///
656/// // Create and display a text box in a single fluent sequence
657/// Boxy::builder()
658/// .box_type(BoxType::Double)
659/// .color("#00ffff")
660/// .padding(BoxPad::uniform(1), BoxPad::from_tldr(2, 2, 1, 1))
661/// .align(BoxAlign::Center)
662/// .add_segment("Hello, Boxy!", "#ffffff", BoxAlign::Center)
663/// .add_line("This is a new line.", "#32CD32")
664/// .add_segment("Another section", "#663399", BoxAlign::Left)
665/// .width(50)
666/// .build()
667/// .display();
668/// ```
669#[derive(Debug, Default)]
670pub struct BoxyBuilder <'a> {
671 type_enum: BoxType,
672 data: Vec<Vec<Cow<'a, str>>>,
673 box_col: String,
674 colors: Vec<Vec<Cow<'a, str>>>,
675 int_padding: BoxPad,
676 ext_padding: BoxPad,
677 align: BoxAlign,
678 seg_align: Vec<BoxAlign>,
679 fixed_width: usize,
680 fixed_height: usize,
681 seg_v_div_ratio: Vec<Vec<usize>>,
682 terminal_width_offset: i32,
683}
684
685impl <'a> BoxyBuilder <'a> {
686 /// Creates a new `BoxyBuilder` with default values.
687 ///
688 /// This creates a builder with the following default values:
689 /// - Box type: `BoxType::Single`
690 /// - Color: empty string (will use white if not set)
691 /// - Padding: zero on all sides
692 /// - Alignment: `BoxAlign::Left`
693 /// - No text segments
694 ///
695 /// # Examples
696 ///
697 /// ```
698 /// use boxy_cli::prelude::*;
699 ///
700 /// let builder = BoxyBuilder::new();
701 /// // Configure the builder with various methods
702 /// let my_box = builder.box_type(BoxType::Double)
703 /// .color("#00ffff")
704 /// .build();
705 /// ```
706 ///
707 /// Typically used through the `Boxy::builder()` factory method:
708 ///
709 ///
710 /// ```
711 /// use boxy_cli::prelude::*;
712 ///
713 /// let builder = Boxy::builder(); // Same as BoxyBuilder::new()
714 /// ```
715 pub fn new() -> Self {
716 Self::default()
717 }
718
719 /// Sets the border type for the text box.
720 ///
721 /// This determines the visual style of the box borders, including the characters used for
722 /// corners, edges, and intersections. Different styles can create different visual effects,
723 /// from simple ASCII-style boxes to double-lined or rounded boxes.
724 ///
725 /// # Arguments
726 ///
727 /// * `box_type` - The border style from the [`BoxType`](../constructs/enum.BoxType.html) enum
728 ///
729 /// # Returns
730 ///
731 /// The builder instance for method chaining
732 ///
733 /// # Examples
734 ///
735 /// ```
736 /// use boxy_cli::prelude::*;
737 ///
738 /// let builder = Boxy::builder()
739 /// .box_type(BoxType::Double); // Use double-lined borders
740 ///
741 /// // Or try other border styles
742 /// let rounded_box = Boxy::builder()
743 /// .box_type(BoxType::Rounded)
744 /// .build();
745 /// ```
746 pub fn box_type(mut self, box_type: BoxType) -> Self {
747 self.type_enum = box_type;
748 self
749 }
750
751 /// Sets the border color for the text box.
752 ///
753 /// This method defines the color of the box borders, including corners, edges, and intersections.
754 /// The color is specified using a hexadecimal color code (e.g. "#00ffff" for cyan).
755 ///
756 /// # Arguments
757 ///
758 /// * `box_color` - A hex color code string (e.g. "#00ffff", "#ff0000")
759 ///
760 /// # Returns
761 ///
762 /// The builder instance for method chaining
763 ///
764 /// # Examples
765 ///
766 /// ```
767 /// use boxy_cli::prelude::*;
768 ///
769 /// // Create a box with cyan borders
770 /// let cyan_box = Boxy::builder()
771 /// .color("#00ffff")
772 /// .build();
773 ///
774 /// // Create a box with red borders
775 /// let red_box = Boxy::builder()
776 /// .color("#ff0000")
777 /// .build();
778 /// ```
779 ///
780 /// # Note
781 ///
782 /// The actual appearance depends on terminal support for colors.
783 pub fn color(mut self, box_color: &str) -> Self {
784 self.box_col = box_color.to_string();
785 self
786 }
787
788 /// Adds a new text segment to the box with specified text, color, and alignment.
789 ///
790 /// Each segment represents a distinct section of the textbox that will be separated by
791 /// horizontal dividers. This method is used to add the first or subsequent major
792 /// sections of content.
793 ///
794 /// # Arguments
795 ///
796 /// * `text` - The text content for this segment
797 /// * `color` - A hex color code (e.g. "#ffffff") for the text color
798 /// * `text_align` - The alignment for this text segment (left, center, right)
799 ///
800 /// # Returns
801 ///
802 /// The builder instance for method chaining
803 ///
804 /// # Examples
805 ///
806 /// ```
807 /// use boxy_cli::prelude::*;
808 ///
809 /// let my_box = Boxy::builder()
810 /// // Add a centered header segment in white
811 /// .add_segment("Header", "#ffffff", BoxAlign::Center)
812 /// // Add a left-aligned content segment in green
813 /// .add_segment("Content goes here", "#00ff00", BoxAlign::Left)
814 /// .build();
815 /// ```
816 pub fn add_segment(mut self, text: &str, color: &str, text_align: BoxAlign) -> Self {
817 self.data.push(vec![Cow::Owned(text.to_owned())]);
818 self.colors.push(vec![Cow::Owned(color.to_owned())]);
819 self.seg_align.push(text_align);
820 self
821 }
822
823 /// Adds a new line of text to the most recently added segment.
824 ///
825 /// This method adds a line of text to the last segment that was created.
826 /// The new line will appear below the existing content in that segment.
827 /// Unlike `add_segment()`, this does not create a new segment with a divider.
828 ///
829 /// # Arguments
830 ///
831 /// * `text` - The text content to add as a new line
832 /// * `color` - A hex color code (e.g. "#ffffff") for the text color
833 ///
834 /// # Returns
835 ///
836 /// The builder instance for method chaining
837 ///
838 /// # Examples
839 ///
840 /// ```
841 /// use boxy_cli::prelude::*;
842 ///
843 /// let my_box = Boxy::builder()
844 /// // Add a header segment
845 /// .add_segment("Header", "#ffffff", BoxAlign::Center)
846 /// // Add a subheader as a new line in the same segment
847 /// .add_line("Subheader text", "#aaaaaa")
848 /// // Add a different segment with a divider
849 /// .add_segment("Content section", "#00ff00", BoxAlign::Left)
850 /// .build();
851 /// ```
852 ///
853 /// # Panics
854 ///
855 /// Panics if no segments have been added yet.
856 pub fn add_line(mut self, text: &str, color: &str) -> Self {
857 if let Some(last_segment) = self.data.last_mut() {
858 last_segment.push(Cow::Owned(text.to_owned()));
859 } else {
860 self.data.push(vec![Cow::Owned(text.to_owned())]);
861 }
862 self.colors[self.data.len()-1].push(Cow::Owned(color.to_owned()));
863 self
864 }
865
866 /// Sets the overall alignment of the text box within the terminal.
867 ///
868 /// This method controls the horizontal positioning of the entire text box relative to the
869 /// terminal width. It does not affect the alignment of text within the box segments,
870 /// which is specified individually when adding segments.
871 ///
872 /// # Arguments
873 ///
874 /// * `alignment` - The alignment to use: `BoxAlign::Left`, `BoxAlign::Center`, or `BoxAlign::Right`
875 ///
876 /// # Returns
877 ///
878 /// The builder instance for method chaining
879 ///
880 /// # Examples
881 ///
882 /// ```
883 /// use boxy_cli::prelude::*;
884 ///
885 /// // Create a box centered in the terminal
886 /// let centered_box = Boxy::builder()
887 /// .align(BoxAlign::Center)
888 /// .add_segment("This box is centered in the terminal", "#ffffff", BoxAlign::Left)
889 /// .build();
890 ///
891 /// // Create a box aligned to the right edge of the terminal
892 /// let right_box = Boxy::builder()
893 /// .align(BoxAlign::Right)
894 /// .add_segment("This box is aligned to the right", "#ffffff", BoxAlign::Left)
895 /// .build();
896 /// ```
897 pub fn align(mut self, alignment: BoxAlign) -> Self {
898 self.align = alignment;
899 self
900 }
901
902 /// Sets the internal padding between the box border and its text content.
903 ///
904 /// Internal padding creates space between the border of the box and the text inside it,
905 /// providing visual breathing room for the content.
906 ///
907 /// # Arguments
908 ///
909 /// * `padding` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance specifying the internal padding values
910 ///
911 /// # Returns
912 ///
913 /// The builder instance for method chaining
914 ///
915 /// # Examples
916 ///
917 /// ```
918 /// use boxy_cli::prelude::*;
919 ///
920 /// // Set uniform internal padding of 2 spaces on all sides
921 /// let padded_box = Boxy::builder()
922 /// .internal_padding(BoxPad::uniform(2))
923 /// .build();
924 ///
925 /// // Set different padding for each side (top, left, bottom, right)
926 /// let custom_pad_box = Boxy::builder()
927 /// .internal_padding(BoxPad::from_tldr(1, 3, 1, 3))
928 /// .build();
929 /// ```
930 pub fn internal_padding(mut self, padding: BoxPad) -> Self {
931 self.int_padding = padding;
932 self
933 }
934
935 /// Sets the external padding between the terminal edges and the text box.
936 ///
937 /// External padding creates space between the edges of the terminal and the border of the box.
938 /// This affects the positioning of the box within the terminal.
939 ///
940 /// # Arguments
941 ///
942 /// * `padding` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance specifying the external padding values
943 ///
944 /// # Returns
945 ///
946 /// The builder instance for method chaining
947 ///
948 /// # Examples
949 ///
950 /// ```
951 /// use boxy_cli::prelude::*;
952 ///
953 /// // Add 5 spaces of external padding on all sides
954 /// let padded_box = Boxy::builder()
955 /// .external_padding(BoxPad::uniform(5))
956 /// .build();
957 ///
958 /// // Add 10 spaces of padding on the left side only
959 /// let left_padded_box = Boxy::builder()
960 /// .external_padding(BoxPad::from_tldr(0, 10, 0, 0))
961 /// .build();
962 /// ```
963 pub fn external_padding(mut self, padding: BoxPad) -> Self {
964 self.ext_padding = padding;
965 self
966 }
967
968 /// Sets both internal and external padding for the text box in a single call.
969 ///
970 /// This is a convenience method that combines setting both external padding (between terminal
971 /// edges and box) and internal padding (between box border and text) in one call.
972 ///
973 /// # Arguments
974 ///
975 /// * `external` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance for the external padding (between terminal edges and box)
976 /// * `internal` - A [`BoxPad`](../constructs/struct.BoxPad.html) instance for the internal padding (between box border and text)
977 ///
978 /// # Returns
979 ///
980 /// The builder instance for method chaining
981 ///
982 /// # Examples
983 ///
984 /// ```
985 /// use boxy_cli::prelude::*;
986 ///
987 /// // Set both padding types at once
988 /// let box_with_padding = Boxy::builder()
989 /// .padding(
990 /// BoxPad::from_tldr(1, 5, 1, 5), // external padding
991 /// BoxPad::uniform(2) // internal padding
992 /// )
993 /// .build();
994 /// ```
995 pub fn padding(mut self, external: BoxPad, internal: BoxPad) -> Self {
996 self.ext_padding = external;
997 self.int_padding = internal;
998 self
999 }
1000
1001 /// Sets a fixed width for the text box instead of dynamically sizing it to the terminal width.
1002 ///
1003 /// By default, the text box automatically adjusts its width based on the terminal size.
1004 /// This method allows you to specify a fixed width instead, which can be useful for
1005 /// creating boxes of consistent size or for controlling the layout of multiple boxes.
1006 ///
1007 /// # Arguments
1008 ///
1009 /// * `width` - The desired width in number of characters (including borders)
1010 ///
1011 /// # Returns
1012 ///
1013 /// The builder instance for method chaining
1014 ///
1015 /// # Examples
1016 ///
1017 /// ```
1018 /// use boxy_cli::prelude::*;
1019 ///
1020 /// // Create a box with a fixed width of 50 characters
1021 /// let fixed_width_box = Boxy::builder()
1022 /// .width(50)
1023 /// .add_segment("This box has a fixed width of 50 characters", "#ffffff", BoxAlign::Left)
1024 /// .build();
1025 /// ```
1026 ///
1027 /// # Note
1028 ///
1029 /// Setting width to 0 returns to dynamic sizing based on terminal width.
1030 pub fn width(mut self, width: usize) -> Self {
1031 self.fixed_width = width;
1032 self
1033 }
1034
1035 /// Sets a fixed height for the text box by adding whitespace above and below the text.
1036 ///
1037 ///
1038 /// # Note
1039 ///
1040 /// This feature is experimental and may not work as expected in the current version.
1041 /// Setting height to 0 returns to dynamic sizing based on content.
1042 ///
1043 ///
1044 /// This method allows you to specify a fixed height for the box, which can be useful for
1045 /// creating boxes of consistent size or for controlling the layout of multiple boxes.
1046 ///
1047 /// # Arguments
1048 ///
1049 /// * `height` - The desired height in number of lines (including borders)
1050 ///
1051 /// # Returns
1052 ///
1053 /// The builder instance for method chaining
1054 ///
1055 /// # Examples
1056 ///
1057 /// ```
1058 /// use boxy_cli::prelude::*;
1059 ///
1060 /// // Create a box with a fixed height of 20 lines
1061 /// let fixed_height_box = Boxy::builder()
1062 /// .height(20)
1063 /// .add_segment("This box has a fixed height", "#ffffff", BoxAlign::Center)
1064 /// .build();
1065 /// ```
1066 ///
1067 pub fn height(mut self, height: usize) -> Self {
1068 self.fixed_height = height;
1069 self
1070 }
1071
1072 /// Sets the size ratios between segments for vertical divisions.
1073 ///
1074 ///
1075 /// # Note
1076 ///
1077 /// This feature is experimental and may not work as expected in the current version.
1078 /// Setting height to 0 returns to dynamic sizing based on content.
1079 ///
1080 ///
1081 /// This method allows you to specify the relative width ratios when dividing a segment
1082 /// vertically into columns. Each number in the `ratios` vector represents the relative
1083 /// width of a column.
1084 ///
1085 /// # Arguments
1086 ///
1087 /// * `seg_index` - The index of the segment to apply the ratios to
1088 /// * `ratios` - A vector of relative width values for each column
1089 ///
1090 /// # Returns
1091 ///
1092 /// The builder instance for method chaining
1093 ///
1094 /// # Examples
1095 ///
1096 /// ```
1097 /// use boxy_cli::prelude::*;
1098 ///
1099 /// // Create a box with a segment that has three columns in a 1:2:1 ratio
1100 /// let columned_box = Boxy::builder()
1101 /// .add_segment("Segment with columns", "#ffffff", BoxAlign::Center)
1102 /// .segment_ratios(0, vec![1, 2, 1])
1103 /// .build();
1104 /// ```
1105 ///
1106 pub fn segment_ratios(mut self, seg_index: usize, ratios: Vec<usize>) -> Self {
1107 if seg_index >= self.seg_v_div_ratio.len() {
1108 self.seg_v_div_ratio.resize(seg_index + 1, Vec::new());
1109 }
1110 self.seg_v_div_ratio[seg_index] = ratios;
1111 self
1112 }
1113
1114 /// Sets the offset used when calculating the dynamic width of the text box based on the terminal size.
1115 ///
1116 ///
1117 /// # Note
1118 ///
1119 /// This feature is experimental and may not work as expected in the current version.
1120 /// Setting height to 0 returns to dynamic sizing based on content.
1121 ///
1122 ///
1123 /// By default, when `fixed_width` is not set, the text box width is calculated as the terminal
1124 /// width minus 20 characters. This method allows you to customize this default offset to make
1125 /// the box wider or narrower relative to the terminal width.
1126 ///
1127 /// # Arguments
1128 ///
1129 /// * `offset` - The number of characters to subtract from the terminal width.
1130 /// Positive values make the box narrower, negative values widen it.
1131 ///
1132 /// # Returns
1133 ///
1134 /// The builder instance for method chaining
1135 ///
1136 /// # Examples
1137 ///
1138 /// ```
1139 /// use boxy_cli::prelude::*;
1140 ///
1141 /// // Make the box 10 characters narrower than the default
1142 /// let narrower_box = Boxy::builder()
1143 /// .set_terminal_width_offset(30) // terminal width - 30
1144 /// .build();
1145 ///
1146 /// // Make the box 10 characters wider than the default
1147 /// let wider_box = Boxy::builder()
1148 /// .set_terminal_width_offset(10) // terminal width - 10
1149 /// .build();
1150 /// ```
1151 ///
1152 /// # Warning
1153 ///
1154 /// Using negative offsets can cause the box to extend beyond the terminal boundaries,
1155 /// which may result in unexpected display issues.
1156 ///
1157 pub fn set_terminal_width_offset(mut self, offset: i32) -> Self {
1158 self.terminal_width_offset = offset;
1159 self
1160 }
1161
1162
1163 /// Builds the `Boxy` instance.
1164 ///
1165 /// ```
1166 /// # use boxy_cli::prelude::*;
1167 /// # let mut my_box = BoxyBuilder::new();
1168 /// my_box.build();
1169 /// ```
1170 /// Subsequently, display using display()
1171 /// ```
1172 /// # use boxy_cli::prelude::*;
1173 /// # let mut my_box = BoxyBuilder::new();
1174 /// my_box.build().display();
1175 /// ```
1176 ///
1177 pub fn build(self) -> Boxy <'a> {
1178 Boxy {
1179 type_enum: self.type_enum,
1180 tot_seg: self.data.len(),
1181 sect_count: self.data.len(),
1182 data: self.data,
1183 box_col: self.box_col,
1184 colors: self.colors,
1185 int_padding: self.int_padding,
1186 ext_padding: self.ext_padding,
1187 align: self.align,
1188 seg_align: self.seg_align,
1189 fixed_width: self.fixed_width,
1190 fixed_height: self.fixed_height,
1191 seg_v_div_count: {
1192 let mut seg_v_div_count = Vec::new();
1193 for seg in &self.seg_v_div_ratio {
1194 seg_v_div_count.push(seg.len());
1195 }
1196 seg_v_div_count
1197 },
1198 seg_v_div_ratio: self.seg_v_div_ratio,
1199 terminal_width_offset: self.terminal_width_offset,
1200
1201 }
1202 }
1203}