rcli_loader/
loading_drawer.rs

1use std::{collections::VecDeque, io::Write, sync::{Arc, Mutex, MutexGuard, OnceLock, RwLock}, vec::Vec};
2
3use crate::{drawer_helper::{print_splitter_line, set_terminal_pos, LoadingColorScheme, Position}, loading_element::LoadingElement, terminal_helper::{get_terminal_size, V2Usz}};
4
5const PROGRESS_CHARS_COUNT: u8 = 8;
6static PROGRESS_CHARS: &'static [char] = &['\u{258F}', '\u{258E}', '\u{258D}', '\u{258C}', '\u{258B}', '\u{258A}', '\u{2589}', '\u{2588}'];
7static _LOADING_DRAWER: OnceLock<Mutex<LoadingDrawer>> = OnceLock::new();
8// Macro to define LOADING_DRAWER as mutex lock from _LOADING_DRAWER
9
10struct LoadingDrawer {
11    list: Vec<Arc<RwLock<LoadingElement>>>,
12    color_scheme: Option<Box<dyn LoadingColorScheme + Send + Sync>>,
13    print_history: VecDeque<String>,
14    loadingbar_anchor_position: Position,
15    max_history: usize,
16}
17#[allow(private_interfaces)]
18fn get_loading_drawer() -> MutexGuard<'static, LoadingDrawer> {
19    _LOADING_DRAWER.get_or_init(||
20        Mutex::new(
21            LoadingDrawer { 
22                list: (Vec::new()),
23                color_scheme: None,
24                print_history: VecDeque::new(),
25                loadingbar_anchor_position: Position::BOTTOM, // TODO: Make setter
26                max_history: 50, // TODO: Make setter
27            }
28        )
29    ).lock().unwrap()
30}
31pub fn set_colorscheme(color_scheme: Box<dyn LoadingColorScheme + Send + Sync>) {
32    get_loading_drawer().color_scheme = Some(color_scheme);
33}
34pub fn set_loadingbar_anchor_position(position: Position) {
35    get_loading_drawer().loadingbar_anchor_position = position; // TODO: Figure if this should also redraw everything, or if we assume that automatically happens
36}
37
38pub fn erase_screen() { // Usually to be used at init
39    println!("\x1B[2J");
40}
41pub fn hide_cursor() { // Implementation specific for consoles, might not work
42    println!("\x1b[?25l");
43}
44pub fn show_cursor() { // Implementation specific for consoles, might not work
45    println!("\x1b[?25h");
46}
47
48pub fn add_loading_element(l_elem: Arc<RwLock<LoadingElement>>) {
49    get_loading_drawer().list.push(l_elem);
50}
51
52
53pub fn rcli_print(print_str: String) {
54    let mut drawer: MutexGuard<'static, LoadingDrawer> = get_loading_drawer();
55    if drawer.print_history.len() > drawer.max_history { drawer.print_history.pop_back(); } // Keep history at constant/max size
56    drawer.print_history.push_front(print_str);
57    drop(drawer);
58    redraw_print_history();
59}
60
61// Future todo note: When making scrolling behaviour, slice the messages whenever window is resized and when a new message is added, so they will be presliced for printing.
62pub fn redraw_print_history() {
63    let drawer: MutexGuard<'static, LoadingDrawer> = get_loading_drawer();
64    let history: &VecDeque<String> = &drawer.print_history;
65    let sz: V2Usz = get_terminal_size();
66    let pos: &Position = &drawer.loadingbar_anchor_position;
67
68    let offset: usize = match pos { // Offset height for printing history, which our terminal cursor must jump to, as to avoid overwriting loading bars
69        Position::BOTTOM => 0,
70        Position::TOP => drawer.list.len() + 1
71    };
72    if sz.y <= drawer.list.len() + 1 { // Accounting for both loading elemenets and splitter line
73        println!("\x1b[1EWindow is too small to print history\x1b[0K"); // Reset cursor to next line and foribly print error, also clear to end of line
74        return;
75    }
76    let mut remaining_height: usize = sz.y - drawer.list.len();
77
78    print_splitter_line(&sz, match pos { Position::BOTTOM => remaining_height, Position::TOP => offset }); // Print either at top or bottom of message "box" depending on the wanted position anchoring
79    remaining_height -= 1;
80
81    // Iteratte over each history element, TODO: feature: this should be line indexed already so we can scroll up, and should not just start at first history element and line
82    'outer: for prt_stmnt in history.iter() { // Note: due to vecdeque, we already iterate from front to back
83        for line in prt_stmnt.lines().rev() {
84            for term_fit_line in line.as_bytes().chunks(sz.x as usize - 1).rev() { // Chunk every line so that we can calculate how many times one "line" would wrap in our console, and adjust the remaining height.
85                set_terminal_pos(V2Usz { x: 0, y: offset + remaining_height });
86                print!("{}\x1b[0K", std::str::from_utf8(term_fit_line).unwrap()); // Convert line back to string or utf8, and clear "rest of line"
87                remaining_height -= 1;
88                if remaining_height == 0 { break 'outer; } // Note: Should this flush as well?
89            };
90        };
91    };
92
93    std::io::stdout().flush().unwrap();
94}
95
96pub fn draw_loader() {
97    let sz: V2Usz = get_terminal_size();
98    let drawer = get_loading_drawer();
99    for (i, elem) in drawer.list.iter().enumerate() {
100        let line = match drawer.loadingbar_anchor_position {
101            Position::TOP => i+1,
102            Position::BOTTOM => sz.y as usize - i // This effectively reverses position of queue when printed
103        };
104        print!("\x1B[{line};{column}H", line=line, column=0);
105        // Minus with two as that the reported screen size is two chars too big and will wrap. WARNING: Can cause errors if screen size is below 2 width?
106        let mut unused_char_count: usize = sz.x as usize - 2; // Defines as usize, as all of the string.len() returns usize, so no bulky conversions later
107
108        // All work on element to release read lock quickly
109        let elem_l = elem.read().unwrap();
110        let progress: usize = elem_l.get_progress();
111        let max: usize = elem_l.get_max();
112        let decimal_progress: f32 = elem_l.get_progress_decimal() as f32; // No reason to store and work on f64, since we do not need that precision here
113        let name: Arc<Box<str>> = elem_l.get_name();
114        
115        // Prepare strings to be printed
116        let progress_chunks_str: String = format!("{progress}/{max} ", // Format the progress first, so we can release elem_l
117                progress=elem_l.format_progress_unit(progress),
118                max=elem_l.format_progress_unit(max)); 
119        let name_str: String = format!("{}: ", name);
120
121        
122        // Printout before char loading block
123        print!("{}", name_str);
124        print!("{progress}", progress = progress_chunks_str);
125        
126        
127        // Update unused character count accorind to everyting printed, and what we expect to print (excpet for block char loading) 
128        unused_char_count -= name.len(); // TODO: Error (attempt to subtract with overflow), if screen is not big enough
129        unused_char_count -= progress_chunks_str.len();
130
131        // Print progress char blocks, after everything has been printed, except for the block char loading
132        let pct_per_char: f32 = 1.0 / unused_char_count as f32;
133        let endchar: char = PROGRESS_CHARS[ ( (decimal_progress%pct_per_char) / pct_per_char * PROGRESS_CHARS_COUNT as f32 ) as usize ];
134        let fillchar_len: usize = (decimal_progress / pct_per_char) as usize;
135        match &drawer.color_scheme {
136            None => print!("{endchar:\u{2588}>fillchar_len$}", endchar = endchar, fillchar_len = fillchar_len ),
137            Some(x) => print!("{col_start}{endchar:\u{2588}>fillchar_len$}\x1b[0m",  endchar = endchar, fillchar_len = fillchar_len, col_start = x.get_char_block_color(&elem_l))
138        }
139        
140        //rcli_print!("test\n{}", "123");
141        
142
143        print!("\x1B[0K"); // Erase from cursor to end of line (Only necessary when whole line is not written!)
144        std::io::stdout().flush().unwrap(); // Flush all commands, since no new line is written
145    }
146}
147