1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
use crate::logging::{Logger, LoggerRefreshItemKind, LoggerTextItem};
use crate::types::ErrBox;
use crate::terminal::read_terminal_event;
use crossterm::event::{Event, KeyCode};

struct MultiSelectData<'a> {
    prompt: &'a str,
    item_hanging_indent: u16,
    items: Vec<(bool, &'a String)>,
    active_index: usize,
}

pub fn show_multi_select(
    logger: &Logger,
    context_name: &str,
    prompt: &str,
    item_hanging_indent: u16,
    items: Vec<(bool, &String)>
) -> Result<Vec<usize>, ErrBox> {
    let mut data = MultiSelectData {
        prompt,
        items,
        item_hanging_indent,
        active_index: 0,
    };

    loop {
        let text_items = render_multi_select(&data);
        logger.set_refresh_item(LoggerRefreshItemKind::Selection, text_items);

        match read_terminal_event()? {
            Event::Key(key_event) => {
                match &key_event.code {
                    KeyCode::Up => {
                        if data.active_index == 0 {
                            data.active_index = data.items.len() - 1;
                        } else {
                            data.active_index -= 1;
                        }
                    },
                    KeyCode::Down => {
                        data.active_index = (data.active_index + 1) % data.items.len();
                    },
                    KeyCode::Char(' ') => {
                        // select an item
                        let mut current_item = data.items.get_mut(data.active_index).unwrap();
                        current_item.0 = !current_item.0;
                    },
                    KeyCode::Enter => {
                        break;
                    },
                    KeyCode::Esc => {
                        logger.remove_refresh_item(LoggerRefreshItemKind::Selection);
                        return err!("Selection cancelled.");
                    }
                    _ => {}
                }
            },
            _ => {
                // cause a refresh anyway
            }
        }
    }
    logger.remove_refresh_item(LoggerRefreshItemKind::Selection);

    logger.log_text_items(&render_complete(&data), context_name, crate::terminal::get_terminal_width());

    // return the selected indexes
    let mut result = Vec::new();
    for (i, (is_selected, _)) in data.items.iter().enumerate() {
        if *is_selected { result.push(i); }
    }
    Ok(result)
}

fn render_multi_select(data: &MultiSelectData) -> Vec<LoggerTextItem> {
    let mut result = Vec::new();
    result.push(LoggerTextItem::Text(data.prompt.to_string()));

    for (i, (is_selected, item_text)) in data.items.iter().enumerate() {
        let mut text = String::new();
        text.push_str(if i == data.active_index {
            ">"
        } else {
            " "
        });
        text.push_str(" [");
        text.push_str(if *is_selected {
            "x"
        } else {
            " "
        });
        text.push_str("] ");
        text.push_str(item_text);

        result.push(LoggerTextItem::HangingText {
            text,
            indent: 7 + data.item_hanging_indent,
        });
    }

    result
}

fn render_complete(data: &MultiSelectData) -> Vec<LoggerTextItem> {
    let mut result = Vec::new();
    if data.items.iter().any(|(is_selected, _)| *is_selected) {
        result.push(LoggerTextItem::Text(data.prompt.to_string()));
        for (is_selected, item_text) in data.items.iter() {
            if *is_selected {
                result.push(LoggerTextItem::HangingText {
                    text: format!(" * {}", item_text),
                    indent: 3 + data.item_hanging_indent,
                });
            }
        }
    }
    result
}