cli_ui/prompt/
select_key.rs1use crate::styles::{paint, DIM, WHITE};
4
5use super::core::{Frame, Key, Prompt, RenderCtx, Step};
6use super::error::{PromptError, Result};
7use super::theme;
8
9#[derive(Clone)]
10struct KeyOption {
11 key: char,
12 label: String,
13 hint: Option<String>,
14}
15
16#[derive(Debug, Clone)]
18pub struct KeySelected {
19 pub key: char,
21 pub label: String,
23}
24
25pub struct SelectKeyPrompt {
27 label: String,
28 options: Vec<KeyOption>,
29 case_sensitive: bool,
30}
31
32pub fn select_key(label: impl Into<String>) -> SelectKeyPrompt {
44 SelectKeyPrompt {
45 label: label.into(),
46 options: Vec::new(),
47 case_sensitive: false,
48 }
49}
50
51impl SelectKeyPrompt {
52 pub fn option(mut self, key: char, label: impl Into<String>) -> Self {
54 self.options.push(KeyOption {
55 key,
56 label: label.into(),
57 hint: None,
58 });
59 self
60 }
61 pub fn hint(mut self, text: impl Into<String>) -> Self {
63 if let Some(last) = self.options.last_mut() {
64 last.hint = Some(text.into());
65 }
66 self
67 }
68 pub fn case_sensitive(mut self, v: bool) -> Self {
70 self.case_sensitive = v;
71 self
72 }
73
74 pub fn run(self) -> Result<KeySelected> {
76 assert!(!self.options.is_empty(), "select_key: no options added");
77 super::core::run(self)
78 }
79}
80
81impl Prompt for SelectKeyPrompt {
82 type Output = KeySelected;
83
84 fn handle(&mut self, key: Key) -> Step<KeySelected> {
85 match key {
86 Key::Char(c) => {
87 let hit = self.options.iter().find(|o| {
88 if self.case_sensitive {
89 o.key == c
90 } else {
91 o.key.eq_ignore_ascii_case(&c)
92 }
93 });
94 match hit {
95 Some(o) => Step::Submit(KeySelected {
96 key: o.key,
97 label: o.label.clone(),
98 }),
99 None => Step::Continue,
100 }
101 }
102 Key::Escape | Key::Interrupt => Step::Cancel,
103 _ => Step::Continue,
104 }
105 }
106
107 fn render(&self, _ctx: RenderCtx) -> Frame {
108 let c = super::settings::colors();
109 let mut f: Frame = Vec::with_capacity(self.options.len() + 4);
110 f.push(theme::label(&self.label));
111 f.push(theme::hint(""));
112 let _ = DIM;
116 for o in &self.options {
117 let badge = paint(c.accent, &format!(" {} ", o.key));
118 let label = paint(WHITE, &o.label);
119 let hint = theme::hint_inline(o.hint.as_deref());
120 f.push(format!(
121 "{} {} {}{}",
122 paint(c.active, "│"),
123 badge,
124 label,
125 hint
126 ));
127 }
128 f.push(theme::hint("press the highlighted key"));
129 f.push(theme::frame_bot(None));
130 f
131 }
132
133 fn render_answered(&self, value: &KeySelected) -> Frame {
134 theme::answered(&self.label, &value.label)
135 .split("\r\n")
136 .map(String::from)
137 .collect()
138 }
139
140 fn run_fallback(self) -> Result<KeySelected> {
141 use std::io::Write;
142 let mut out = std::io::stderr();
143 writeln!(out, " {}", self.label).map_err(PromptError::Io)?;
144 for o in &self.options {
145 writeln!(out, " [{}] {}", o.key, o.label).map_err(PromptError::Io)?;
146 }
147 write!(out, " Press a key (and Enter): ").map_err(PromptError::Io)?;
148 out.flush().map_err(PromptError::Io)?;
149 let line = super::engine::fallback::read_line_raw()?;
150 let ch = line.chars().next().ok_or(PromptError::Interrupted)?;
151 let hit = self
152 .options
153 .iter()
154 .find(|o| {
155 if self.case_sensitive {
156 o.key == ch
157 } else {
158 o.key.eq_ignore_ascii_case(&ch)
159 }
160 })
161 .ok_or(PromptError::Interrupted)?;
162 Ok(KeySelected {
163 key: hit.key,
164 label: hit.label.clone(),
165 })
166 }
167}