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