boxy_cli/
boxer.rs

1use colored::Colorize;
2use hex_color::HexColor;
3use crate::templates::*;
4use crate::constructs::*;
5
6
7//TODO: Add Documentation and exaples for each method and/or segment. Especially the display method, the setters and the macro 
8
9
10/// The main struct. contains all the data rekevant to the TextBox
11#[derive(Debug)]
12pub struct Boxy {
13    pub type_enum: BoxType,
14    pub data : Vec<Vec<String>>,
15    pub sect_count: usize,
16    pub box_col : String,
17    pub colors : Vec<Vec<String>>,
18    pub int_padding: BoxPad,
19    pub ext_padding: BoxPad,
20    pub align : BoxAlign,
21    pub fixed_width: usize,
22    pub fixed_height: usize,
23    pub seg_v_div_count: Vec<usize>,
24    pub seg_v_div_ratio: Vec<Vec<usize>>,
25    pub tot_seg: usize,
26}
27
28// Default struct values for the textbox
29impl Default for Boxy {
30    fn default() -> Self {
31        Self {
32            type_enum: BoxType::Single,
33            data : Vec::<Vec<String>>::new(),
34            sect_count: 0usize,
35            box_col: "#ffffff".to_string(),
36            colors : Vec::<Vec<String>>::new(),
37            int_padding: BoxPad::new(),
38            ext_padding: BoxPad::new(),
39            align : BoxAlign::Left,
40            fixed_width: 0usize,
41            fixed_height: 0usize,
42            seg_v_div_count: Vec::<usize>::new(),
43            seg_v_div_ratio: Vec::<Vec<usize>>::new(),
44            tot_seg: 0usize,
45        }
46    }
47}
48
49
50impl Boxy {
51    /// Retuns a new instance of the Boxy struct with specified border type and colour
52    pub fn new(box_type: BoxType, box_color : &str) -> Self {
53        Boxy{
54            type_enum: box_type,
55            data : Vec::<Vec<String>>::new(),
56            sect_count: 0usize,
57            box_col : (&box_color).to_string(),
58            colors : Vec::<Vec<String>>::new(),
59            int_padding: BoxPad::new(),
60            ext_padding: BoxPad::new(),
61            align : BoxAlign::Left,
62            fixed_width: 0usize,
63            fixed_height: 0usize,
64            seg_v_div_count: Vec::<usize>::new(),
65            seg_v_div_ratio: Vec::<Vec<usize>>::new(),
66            tot_seg: 0usize,
67        }
68    }
69
70    /// Adds a new text segment/section to the textbox, separated by a horizontal divider.
71
72    // Adding a new text segment/section to the textbox
73    // also initializes the textbox with its first use -> adds main body text
74    pub fn add_text_sgmt(&mut self, data_string : &str, color : &str) {
75        self.data.push(vec![data_string.to_owned()]);
76        self.colors.push(vec![String::from(color)]);
77        self.sect_count+=1;
78    }
79
80    /// Adds a new text line to the segemnt with a specific index.
81    // Adding a text line to a segemnt with a specific index
82    pub fn add_text_line_indx(&mut self, data_string : &str, seg_index : usize) {
83        self.data[seg_index].push(data_string.to_owned());
84    }
85    
86    /// Adds a new text line to the latest segment.
87    // Adding a text line to the latest segment
88    pub fn add_text_line(&mut self, data_string : &str) {
89        self.data[self.sect_count-1].push(String::from(data_string));
90    }
91
92    /// Sets the aligment of the text in the textbox.
93    // Setting the Alignment maually
94    pub fn set_align(&mut self, align: BoxAlign) {
95        self.align = align;
96    }
97
98    /// Set the internal padding for the textbox. (Between border and text)
99    ///
100    /// !! provide a [`BoxPad`] Struct for the padding
101    // Setting the Padding manually
102    pub fn set_int_padding(&mut self, int_padding : BoxPad) {
103        self.int_padding = int_padding;
104    }
105    /// Set the external padding for the textbox. (Between terminal limits and border)
106    /// 
107    /// !! provide a [`BoxPad`] Struct for the padding
108    pub fn set_ext_padding(&mut self, ext_padding : BoxPad) {
109        self.ext_padding = ext_padding;
110    }
111    /// Set the internal padding and external padding for the textbox.
112    ///
113    /// !! provide a [`BoxPad`] Struct for the padding
114    pub fn set_padding(&mut self, ext_padding : BoxPad, int_padding : BoxPad) {
115        self.int_padding = int_padding;
116        self.ext_padding = ext_padding;
117    }
118
119    /// Sets a fixed width for the textbox insted of dynamically sizing it to the width of the terminal
120    // Setting the Width manually
121    pub fn set_width(&mut self, width : usize) {
122        self.fixed_width = width;
123    }
124
125    /// Sets a fixed height for the textbox. (adds in whitespace above and below the text)
126    ///
127    /// !! This feature is a work in progress. it may not work with the current version of the crate
128    // Setting the Height manually
129    pub fn set_height(&mut self, height : usize) {
130        self.fixed_height = height;
131    }
132
133    /// Sets the size-ratio between segments when using vertical divisions
134    ///
135    /// !! This feature is a work in progress. it may not work with the current version of the crate
136    pub fn set_segment_ratios(&mut self, seg_index: usize, ratios: Vec<usize>) {
137        if seg_index >= self.seg_v_div_ratio.len() {
138            self.seg_v_div_ratio.resize(seg_index + 1, Vec::new());
139        }
140        self.seg_v_div_ratio[seg_index] = ratios;
141    }
142
143    /// Prints/Displays the textbox into the CLI
144    // Main Display Function to display the textbox
145    pub fn display(&mut self) {
146        // Initialising Display Variables
147        let disp_width = if self.fixed_width !=0 {
148            self.fixed_width
149        } else {
150            let size = termsize::get();
151            if size.is_some() {
152                size.unwrap().cols as usize - 20
153            } else {
154                return;
155            }
156        };
157
158        let col_truevals = HexColor::parse(&self.box_col).unwrap();
159        let box_pieces = map_box_type(&self.type_enum);
160        let horiz =box_pieces.horizontal.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b);
161
162        // Printing the top segment
163        print!("{:>width$}", box_pieces.top_left.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b), width=self.ext_padding.left+1);
164        for _ in 0..(disp_width -2*self.ext_padding.right) {
165            print!("{}", horiz);
166        }
167        println!("{}", box_pieces.top_right.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b));
168
169        // Iteratively print all the textbox sections, with appropriate dividers in between
170        for i in 0..self.sect_count {
171            if i > 0 {
172                self.print_h_divider(&col_truevals,  &disp_width);
173            }
174            self.display_segment(i, &disp_width);
175        }
176
177        // Printing bottom segment
178        print!("{:>width$}", box_pieces.bottom_left.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b), width=self.ext_padding.left+1);
179        for _ in 0..disp_width -2*self.ext_padding.right {
180            print!("{}", horiz);
181        }
182        println!("{}", box_pieces.bottom_right.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b));
183
184    }
185
186    // Displaying each individual segment body
187    fn display_segment(&mut self, seg_index: usize, disp_width: &usize) {
188
189        // TODO: Add functionality to create segments while displaying the textbox i.e. columns
190        let col_truevals = HexColor::parse(&self.box_col).unwrap();
191
192        // Loop for all text lines
193        for i in 0..self.data[seg_index].len() {
194            // Processing data
195            let mut processed_data = String::with_capacity(self.data[seg_index][i].len()+1);
196            processed_data.push_str(self.data[seg_index][i].trim());
197            processed_data.push(' ');
198            let mut ws_indices = Vec::new();
199            // Creating a map of all whitespaces to help in text wrapping for this text segment\
200            // looping over binary segments, as all other methods create a new iterator, taking up more mem
201            let mut k = 0usize;
202            while k < processed_data.len() {
203                if processed_data.as_bytes()[k] == b' ' {
204                    ws_indices.push(k);
205                }
206                k += 1;
207            }
208
209            let liner: Vec<String> = text_wrap_vec(&processed_data, &mut ws_indices, &disp_width.clone(), &self.ext_padding, &self.int_padding);
210
211            // Actually printing shiet
212
213            // Iterative printing. migrated form recursive to prevent stack overflows and reduce complexity, also to improve code efficiency
214            iter_line_prnt(&liner, map_box_type(&self.type_enum), &col_truevals, disp_width, &self.ext_padding, &self.int_padding, &self.align);
215
216            // printing an empty line between consecutive non-terminal text line
217            if i < self.data[seg_index].len() - 1 {
218                println!("{1:>width$}{}{1}", " ".repeat(disp_width - self.ext_padding.lr()),
219                         map_box_type(&self.type_enum)
220                             .vertical.to_string()
221                             .truecolor(col_truevals.r, col_truevals.g, col_truevals.b),
222                         width=self.ext_padding.left+1);
223            }
224        }
225        // Recursive Printing of text -> now depreciated
226        // recur_whitespace_printing(&processed_data, &mut ws_indices, &self.type_enum, &terminal_size, 0usize, &col_truevals, &self.ext_padding, &self.int_padding, &self.align);
227    }
228
229    // Printing the horizontal divider.
230    fn print_h_divider(&mut self, boxcol: &HexColor, disp_width: &usize){
231        let box_pieces = map_box_type(&self.type_enum);
232        let horiz =  box_pieces.horizontal.to_string().truecolor(boxcol.r, boxcol.g, boxcol.b);
233        print!("{:>width$}", box_pieces.left_t.to_string().truecolor(boxcol.r, boxcol.g, boxcol.b), width=self.ext_padding.left+1);
234        for _ in 0..*disp_width-self.ext_padding.lr() {
235            print!("{}", horiz);
236        }
237        println!("{}", box_pieces.right_t.to_string().truecolor(boxcol.r, boxcol.g, boxcol.b));
238    }
239
240    //TODO: Set up the boxy struct to have a vec for each segment, with a cons list for each individual segment
241    // that'll make sure that all segments have a proper implementation, and no segments have mismatched data datatypes
242    
243    //TODO: have the function fetch the data for it's sub-segment only, and not repeat the same for all the segments
244
245    //TODO: Resolve the internal whiitespace padding issue which occurs when longer segments are placed beside shorter segments
246    //TODO: check if the whitespace padidng is uniform along the length, if not squish the bug.
247    
248    //TODO: Not kill yourself while doing this
249
250    // Display a segment divided into mini-segments based on ratios
251    fn _display_segment_with_ratios(&mut self, seg_index: usize, terminal_size: &usize) {
252        let col_truevals = HexColor::parse(&self.box_col).unwrap();
253        let box_pieces = map_box_type(&self.type_enum);
254
255        // Fetch ratios for the segment
256        let ratios = if seg_index < self.seg_v_div_ratio.len() {
257            &self.seg_v_div_ratio[seg_index]
258        } else {
259            static EMPTY_VEC: Vec<usize> = Vec::new();
260            &EMPTY_VEC
261        };
262
263        if ratios.is_empty() {
264            // If no ratios are defined, fallback to a single segment
265            self.display_segment(seg_index, terminal_size);
266            return;
267        }
268
269        // Calculate total ratio and widths for each mini-segment
270        let total_ratio: usize = ratios.iter().sum();
271        let printable_width = terminal_size - self.ext_padding.lr();
272        let segment_widths: Vec<usize> = ratios.iter().map(|r| r * printable_width / total_ratio).collect();
273
274        // Prepare text for each mini-segment
275        let mut mini_segments: Vec<Vec<String>> = vec![Vec::new(); ratios.len()];
276        let lines = &self.data[seg_index];
277        for line in lines.iter() {
278            let mut processed_data = String::with_capacity(line.len() + 1);
279            processed_data.push_str(line.trim());
280            processed_data.push(' ');
281
282            let mut ws_indices = Vec::new();
283            let mut k = 0usize;
284            while k < processed_data.len() {
285                if processed_data.as_bytes()[k] == b' ' {
286                    ws_indices.push(k);
287                }
288                k += 1;
289            }
290
291            // Distribute text into mini-segments
292            for (j, width) in segment_widths.iter().enumerate() {
293                let liner = text_wrap_vec(&processed_data, &mut ws_indices, width, &self.ext_padding, &self.int_padding);
294                mini_segments[j].extend(liner);
295            }
296        }
297
298        // Print each line of the mini-segments with vertical dividers
299        let max_lines = mini_segments.iter().map(|seg| seg.len()).max().unwrap_or(0);
300        for line_index in 0..max_lines {
301            // Print the left padding and vertical bar
302            print!("{:>width$}", box_pieces.vertical.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b), width = self.ext_padding.left + 1);
303
304            for (j, segment) in mini_segments.iter().enumerate() {
305                if line_index < segment.len() {
306                    // Print the text in the mini-segment
307                    print!("{:<pad$}", " ", pad = self.int_padding.left);
308                    print!("{:<width$}", segment[line_index], width = segment_widths[j] - self.int_padding.lr());
309                    print!("{:<pad$}", " ", pad = self.int_padding.right);
310                } else {
311                    // Print empty space if no text exists for this line
312                    print!("{:<width$}", " ", width = segment_widths[j]);
313                }
314
315                // Print vertical divider between mini-segments
316                if j < mini_segments.len() - 1 {
317                    print!("{}", box_pieces.vertical.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b));
318                }
319            }
320
321            // Print the right padding and vertical bar
322            println!("{}", box_pieces.vertical.to_string().truecolor(col_truevals.r, col_truevals.g, col_truevals.b));
323        }
324    }
325}
326
327// Function to find the next-most-fitting string slice for the give terminal size
328
329fn nearest_whitespace(map: &mut Vec<usize>, printable_length: &usize, start_index: usize) -> usize {
330    let mut next_ws = 0;
331    for i in map {
332        if *i > start_index && *i-start_index <= *printable_length {
333            next_ws = *i;
334        }
335    }
336    // force line break if no appropriate whitespace found
337    if next_ws == 0 {
338        next_ws = start_index + printable_length;
339    }
340    next_ws
341}
342
343// Recursively printing the next text segment into the textbox
344
345// Went with recursive as that is just more modular, and I can just reuse this code for printing horizontal and vertical segments.
346
347fn text_wrap_vec(data:&str, map: &mut Vec<usize>, disp_width: &usize, ext_padding: &BoxPad, int_padding: &BoxPad) -> Vec<String> {
348    let mut liner: Vec<String> = Vec::new();
349    let mut start_index = 0;
350
351    while start_index < data.len() {
352        let next_ws = nearest_whitespace(map, &(disp_width - (int_padding.lr() + ext_padding.lr()) - 2), start_index);
353        liner.push(data[start_index..next_ws].to_string());
354        if next_ws >= data.len()-1 {break;}
355        start_index = next_ws+1;
356    }
357    liner
358
359    // Legacy recursive code. Depreciated to increase efficiency for larger use cases
360    /*
361    let next_ws = nearest_whitespace(map, &(term_size - 2*(int_padding + ext_padding)), start_index);
362    line_vec.push(String::from(&data[start_index..next_ws]));
363    if next_ws < (data.len()-1) {
364        text_wrap_vec(data, map, term_size, next_ws+1, ext_padding, int_padding, line_vec);
365    }
366    */
367}
368
369
370fn iter_line_prnt(liner : &[String], box_pieces:BoxTemplates, box_col: &HexColor, disp_width: &usize, ext_padding: &BoxPad, int_padding: &BoxPad, align: &BoxAlign) {
371    let printable_area = disp_width - (int_padding.lr() + ext_padding.lr());
372    let vertical = box_pieces.vertical.to_string().truecolor(box_col.r, box_col.g, box_col.b);
373    match align {
374        BoxAlign::Left => {
375            for i in liner.iter() {
376                print!("{:>width$}", vertical, width=ext_padding.left+1);
377                print!("{:<pad$}", " ", pad=int_padding.left);
378                print!("{:<width$}", i, width=printable_area-2); // subtract 2 for the bars
379                print!("{:<pad$}", " ", pad=int_padding.right);
380                println!("{}", vertical);
381            }
382        },
383        BoxAlign::Center => {
384            for i in liner.iter() {
385                print!("{:>width$}", vertical, width=ext_padding.left+1);
386                print!("{:<pad$}", " ", pad=int_padding.left+((printable_area-i.len())/2));
387                print!("{}", i);
388                print!("{:<pad$}", " ", pad=int_padding.right+(printable_area-i.len())-((printable_area-i.len())/2));
389                println!("{}", vertical);
390            }
391        },
392        BoxAlign::Right => {
393            for i in liner.iter() {
394                print!("{:>width$}", vertical, width=ext_padding.left+1);
395                print!("{:<pad$}", " ", pad=int_padding.left);
396                print!("{:>width$}", i, width=printable_area-2); // subtract 2 for the bars
397                print!("{:<pad$}", " ", pad=int_padding.right);
398                println!("{}", vertical);
399            }
400        }
401    }
402}
403
404// returns the box template for the given enum
405fn map_box_type (boxtype : &BoxType) -> BoxTemplates{
406    match boxtype {
407        BoxType::Classic => CLASSIC_TEMPLATE,
408        BoxType::Single => SINGLE_TEMPLATE,
409        BoxType::DoubleHorizontal => DOUB_H_TEMPLATE,
410        BoxType::DoubleVertical => DOUB_V_TEMPLATE,
411        BoxType::Double => DOUBLE_TEMPLATE,
412        BoxType::Bold => BOLD_TEMPLATE,
413        BoxType::Rounded => ROUNDED_TEMPLATE,
414        BoxType::BoldCorners => BOLD_CORNERS_TEMPLATE,
415    }
416}
417
418// Macro type resolution fucntions for boxy!
419
420
421/// Macro type-resolution function
422pub fn resolve_col(dat : String) -> String {
423    dat
424}
425/// Macro type-resolution function
426pub fn resolve_pad(dat : String) -> BoxPad {
427    BoxPad::uniform(dat.parse::<usize>().unwrap_or(0usize))
428}
429/// Macro type-resolution function
430pub fn resolve_align(dat : String) -> BoxAlign {
431    match &*dat {
432        "center" => BoxAlign::Center,
433        "right" => BoxAlign::Right,
434        "left" => BoxAlign::Left,
435        _ => BoxAlign::Left,
436    }
437}
438/// Macro type-resolution function
439pub fn resolve_type(dat : String) -> BoxType{
440    match &*dat {
441        "classic" => BoxType::Classic,
442        "single" => BoxType::Single,
443        "double_horizontal" => BoxType::DoubleHorizontal,
444        "double_vertical" => BoxType::DoubleVertical,
445        "double" => BoxType::Double,
446        "bold" => BoxType::Bold,
447        "rounded" => BoxType::Rounded,
448        "bold_corners" => BoxType::BoldCorners,
449        _ => BoxType::Single,
450    }
451}
452/// Macro type-resolution function
453pub fn resolve_segments(dat : String) -> usize {
454    dat.parse().expect("failed to parse total segment number")
455}