1use super::core::{Frame, Key, Prompt, RenderCtx, Step};
4use super::error::{PromptError, Result};
5use super::limit_options::limit_options;
6use super::theme;
7
8#[derive(Clone)]
9struct SelectOption {
10 value: String,
11 label: String,
12 hint: Option<String>,
13}
14
15#[derive(Debug, Clone)]
18pub struct Selected {
19 pub index: usize,
21 pub value: String,
24 pub label: String,
26}
27
28impl Selected {
29 pub fn index(&self) -> usize {
31 self.index
32 }
33 pub fn value(&self) -> &str {
35 &self.value
36 }
37 pub fn label(&self) -> &str {
39 &self.label
40 }
41}
42
43pub struct SelectPrompt {
45 label: String,
46 options: Vec<SelectOption>,
47 prompt_hint: Option<String>,
48 max_items: usize,
49 pos: usize,
50}
51
52pub fn select(label: impl Into<String>) -> SelectPrompt {
64 SelectPrompt {
65 label: label.into(),
66 options: Vec::new(),
67 prompt_hint: None,
68 max_items: 10,
69 pos: 0,
70 }
71}
72
73impl SelectPrompt {
74 pub fn option(mut self, value: impl Into<String>, label: impl Into<String>) -> Self {
78 self.options.push(SelectOption {
79 value: value.into(),
80 label: label.into(),
81 hint: None,
82 });
83 self
84 }
85 pub fn hint(mut self, text: impl Into<String>) -> Self {
88 if let Some(last) = self.options.last_mut() {
89 last.hint = Some(text.into());
90 }
91 self
92 }
93 pub fn prompt_hint(mut self, text: impl Into<String>) -> Self {
95 self.prompt_hint = Some(text.into());
96 self
97 }
98 pub fn default(mut self, index: usize) -> Self {
100 self.pos = index;
101 self
102 }
103 pub fn max_items(mut self, n: usize) -> Self {
106 self.max_items = n.max(1);
107 self
108 }
109
110 pub fn run(mut self) -> Result<Selected> {
112 assert!(!self.options.is_empty(), "select: no options added");
113 self.pos = self.pos.min(self.options.len() - 1);
114 super::core::run(self)
115 }
116}
117
118impl Prompt for SelectPrompt {
119 type Output = Selected;
120
121 fn handle(&mut self, key: Key) -> Step<Selected> {
122 let vim = super::settings::get().vim_keys;
123 let last = self.options.len() - 1;
124 match key {
125 Key::Up | Key::Char('k') if vim => {
126 if self.pos > 0 {
127 self.pos -= 1;
128 }
129 Step::Continue
130 }
131 Key::Down | Key::Char('j') if vim => {
132 if self.pos < last {
133 self.pos += 1;
134 }
135 Step::Continue
136 }
137 Key::Up => {
138 if self.pos > 0 {
139 self.pos -= 1;
140 }
141 Step::Continue
142 }
143 Key::Down => {
144 if self.pos < last {
145 self.pos += 1;
146 }
147 Step::Continue
148 }
149 Key::Char('q') if vim => Step::Cancel,
150 Key::Enter => {
151 let opt = &self.options[self.pos];
152 Step::Submit(Selected {
153 index: self.pos,
154 value: opt.value.clone(),
155 label: opt.label.clone(),
156 })
157 }
158 Key::Escape | Key::Interrupt => Step::Cancel,
159 _ => Step::Continue,
160 }
161 }
162
163 fn render(&self, _ctx: RenderCtx) -> Frame {
164 let mut f: Frame = Vec::with_capacity(self.max_items + 4);
165 f.push(theme::label(&self.label));
166 if let Some(ref h) = self.prompt_hint {
167 f.push(theme::hint(h));
168 }
169 f.push(theme::hint(""));
170 let vp = limit_options(self.options.len(), self.pos, self.max_items);
171 if vp.above > 0 {
172 f.push(theme::hint(&format!("↑ {} more", vp.above)));
173 }
174 for i in vp.start..vp.end {
175 let opt = &self.options[i];
176 f.push(theme::cursor(
177 i == self.pos,
178 &opt.label,
179 opt.hint.as_deref(),
180 ));
181 }
182 if vp.below > 0 {
183 f.push(theme::hint(&format!("↓ {} more", vp.below)));
184 }
185 f.push(theme::frame_bot(None));
186 f
187 }
188
189 fn render_answered(&self, value: &Selected) -> Frame {
190 theme::answered(&self.label, &value.label)
191 .split("\r\n")
192 .map(String::from)
193 .collect()
194 }
195
196 fn run_fallback(self) -> Result<Selected> {
197 use std::io::Write;
198 let mut out = std::io::stderr();
199 writeln!(out, " {}", self.label).map_err(PromptError::Io)?;
200 for (i, opt) in self.options.iter().enumerate() {
201 match &opt.hint {
202 Some(h) => writeln!(out, " {}. {} ({})", i + 1, opt.label, h),
203 None => writeln!(out, " {}. {}", i + 1, opt.label),
204 }
205 .map_err(PromptError::Io)?;
206 }
207 loop {
208 write!(out, " Enter number (1–{}): ", self.options.len()).map_err(PromptError::Io)?;
209 out.flush().map_err(PromptError::Io)?;
210 let line = super::engine::fallback::read_line_raw()?;
211 if let Ok(n) = line.parse::<usize>() {
212 if n >= 1 && n <= self.options.len() {
213 let opt = &self.options[n - 1];
214 return Ok(Selected {
215 index: n - 1,
216 value: opt.value.clone(),
217 label: opt.label.clone(),
218 });
219 }
220 }
221 writeln!(
222 out,
223 " Please enter a number between 1 and {}.",
224 self.options.len()
225 )
226 .map_err(PromptError::Io)?;
227 }
228 }
229}