1use rskit_errors::AppResult;
5
6use super::{
7 cancelled, closed_input, focus_down, focus_up, notice, parse_index, with_raw_mode, write_answer,
8};
9use crate::prompt::choice::{Choice, ChoiceId};
10use crate::prompt::key::Key;
11use crate::prompt::mode::PromptMode;
12use crate::prompt::render::{self, Style};
13use crate::prompt::terminal::Terminal;
14
15pub fn run(
17 terminal: &mut (impl Terminal + ?Sized),
18 style: Style,
19 mode: PromptMode,
20 prompt: &str,
21 choices: &[Choice],
22) -> AppResult<Vec<ChoiceId>> {
23 if choices.is_empty() {
24 return Err(rskit_errors::AppError::invalid_input(
25 "prompt",
26 format!("multi_select requires at least one choice: {prompt}"),
27 ));
28 }
29 let defaults: Vec<usize> = choices
30 .iter()
31 .enumerate()
32 .filter_map(|(index, choice)| choice.is_recommended().then_some(index))
33 .collect();
34
35 if !mode.is_interactive() {
36 return Ok(ids(choices, &defaults));
37 }
38
39 if terminal.capabilities().is_key_driven() {
40 key_driven(terminal, style, prompt, choices, defaults)
41 } else {
42 line_driven(terminal, style, prompt, choices, &defaults)
43 }
44}
45
46fn ids(choices: &[Choice], indices: &[usize]) -> Vec<ChoiceId> {
47 indices.iter().map(|&i| choices[i].id().clone()).collect()
48}
49
50fn key_driven(
51 terminal: &mut (impl Terminal + ?Sized),
52 style: Style,
53 prompt: &str,
54 choices: &[Choice],
55 defaults: Vec<usize>,
56) -> AppResult<Vec<ChoiceId>> {
57 let len = choices.len();
58 let mut cursor = 0;
59 let mut selected = defaults;
60 selected.sort_unstable();
61 with_raw_mode(terminal, |terminal| {
62 let mut drawn = draw(terminal, style, prompt, choices, cursor, &selected)?;
63 loop {
64 let key = terminal.read_key()?;
65 match key {
66 Key::Enter => return Ok(ids(choices, &selected)),
67 Key::Escape | Key::Interrupt => return Err(cancelled(prompt)),
68 Key::Up => cursor = focus_up(cursor, len),
69 Key::Down | Key::Tab => cursor = focus_down(cursor, len),
70 Key::Home => cursor = 0,
71 Key::End => cursor = len - 1,
72 Key::Space => toggle(&mut selected, cursor),
73 _ => continue,
74 }
75 terminal.clear_last_lines(drawn)?;
76 drawn = draw(terminal, style, prompt, choices, cursor, &selected)?;
77 }
78 })
79}
80
81fn toggle(selected: &mut Vec<usize>, index: usize) {
82 if let Some(position) = selected.iter().position(|&i| i == index) {
83 selected.remove(position);
84 } else {
85 selected.push(index);
86 selected.sort_unstable();
87 }
88}
89
90fn draw(
91 terminal: &mut (impl Terminal + ?Sized),
92 style: Style,
93 prompt: &str,
94 choices: &[Choice],
95 cursor: usize,
96 selected: &[usize],
97) -> AppResult<u16> {
98 terminal.write_line(&render::heading(style, prompt))?;
99 for row in render::frame_rows(style, choices, cursor, Some(selected), None) {
100 terminal.write_line(&row)?;
101 }
102 terminal.write_line(&render::key_hint(style, true))?;
103 terminal.flush()?;
104 Ok(u16::try_from(choices.len())
105 .unwrap_or(u16::MAX)
106 .saturating_add(2))
107}
108
109fn line_driven(
110 terminal: &mut (impl Terminal + ?Sized),
111 style: Style,
112 prompt: &str,
113 choices: &[Choice],
114 defaults: &[usize],
115) -> AppResult<Vec<ChoiceId>> {
116 terminal.write_line(&render::heading(style, prompt))?;
117 for row in render::numbered_rows(style, choices, true, None) {
118 terminal.write_line(&row)?;
119 }
120 loop {
121 write_answer(terminal, style, Some(&default_hint(defaults)))?;
122 let Some(line) = terminal.read_line()? else {
123 return Err(closed_input(prompt));
124 };
125 let answer = line.trim();
126 if answer.is_empty() {
127 return Ok(ids(choices, defaults));
128 }
129 if let Some(indices) = parse_indices(answer, choices.len()) {
130 return Ok(ids(choices, &indices));
131 }
132 notice(
133 terminal,
134 style,
135 &format!(
136 "enter comma-separated numbers between 1 and {}",
137 choices.len()
138 ),
139 )?;
140 }
141}
142
143fn default_hint(defaults: &[usize]) -> String {
144 if defaults.is_empty() {
145 return "[none]".to_string();
146 }
147 let list = defaults
148 .iter()
149 .map(|index| (index + 1).to_string())
150 .collect::<Vec<_>>()
151 .join(",");
152 format!("[{list}]")
153}
154
155fn parse_indices(input: &str, len: usize) -> Option<Vec<usize>> {
156 let mut indices = Vec::new();
157 for token in input.split(',') {
158 let index = parse_index(token.trim(), len)?;
159 if !indices.contains(&index) {
160 indices.push(index);
161 }
162 }
163 Some(indices)
164}