cli_ui/prompt/
multiline.rs1use crate::styles::{paint, DIM};
4
5use super::core::{Frame, Key, Prompt, RenderCtx, Step};
6use super::error::{PromptError, Result};
7use super::theme;
8use super::validate::Validator;
9
10pub struct MultilinePrompt {
12 label: String,
13 placeholder: Option<String>,
14 show_submit: bool,
15 validate: Option<Validator>,
16 buf: Vec<String>,
17 focus_submit: bool,
18}
19
20pub fn multiline(label: impl Into<String>) -> MultilinePrompt {
34 MultilinePrompt {
35 label: label.into(),
36 placeholder: None,
37 show_submit: false,
38 validate: None,
39 buf: vec![String::new()],
40 focus_submit: false,
41 }
42}
43
44impl MultilinePrompt {
45 pub fn placeholder(mut self, v: impl Into<String>) -> Self {
47 self.placeholder = Some(v.into());
48 self
49 }
50 pub fn show_submit(mut self, v: bool) -> Self {
53 self.show_submit = v;
54 self
55 }
56 pub fn validate<F>(mut self, f: F) -> Self
58 where
59 F: Fn(&str) -> std::result::Result<(), String> + Send + Sync + 'static,
60 {
61 self.validate = Some(Validator::new(f));
62 self
63 }
64 pub fn rule(mut self, v: Validator) -> Self {
66 self.validate = Some(v);
67 self
68 }
69
70 #[deprecated(since = "0.1.0", note = "use `.rule(...)` instead")]
72 pub fn validate_with(self, v: Validator) -> Self {
73 self.rule(v)
74 }
75
76 pub fn run(self) -> Result<String> {
78 super::core::run(self)
79 }
80
81 fn try_submit(&self) -> Option<String> {
82 let trailing_empty = self.buf.last().map(|s| s.is_empty()).unwrap_or(false)
83 && self.buf.len() >= 2
84 && self.buf[self.buf.len() - 2].is_empty();
85 if !(self.focus_submit || trailing_empty) {
86 return None;
87 }
88 let mut joined: Vec<&str> = self.buf.iter().map(String::as_str).collect();
89 if !self.focus_submit {
90 joined.truncate(joined.len() - 2);
91 }
92 Some(joined.join("\n"))
93 }
94}
95
96impl Prompt for MultilinePrompt {
97 type Output = String;
98
99 fn handle(&mut self, key: Key) -> Step<String> {
100 match key {
101 Key::Char('\t') if self.show_submit => {
102 self.focus_submit = !self.focus_submit;
103 Step::Continue
104 }
105 Key::Char(c) => {
106 self.focus_submit = false;
107 self.buf.last_mut().unwrap().push(c);
108 Step::Continue
109 }
110 Key::Backspace => {
111 if let Some(last) = self.buf.last_mut() {
112 if last.pop().is_none() && self.buf.len() > 1 {
113 self.buf.pop();
114 }
115 }
116 Step::Continue
117 }
118 Key::Enter => {
119 if let Some(value) = self.try_submit() {
120 if let Some(ref v) = self.validate {
121 if let Err(msg) = v.check(&value) {
122 return Step::Reject(msg);
123 }
124 }
125 return Step::Submit(value);
126 }
127 self.buf.push(String::new());
128 Step::Continue
129 }
130 Key::Escape | Key::Interrupt => Step::Cancel,
131 _ => Step::Continue,
132 }
133 }
134
135 fn render(&self, _ctx: RenderCtx) -> Frame {
136 let mut f: Frame = Vec::with_capacity(self.buf.len() + 4);
137 f.push(theme::label(&self.label));
138 let placeholder = self.placeholder.as_deref().unwrap_or("");
139 let is_empty = self.buf.len() == 1 && self.buf[0].is_empty();
140 if is_empty && !placeholder.is_empty() {
141 f.push(format!("{} {}", paint(DIM, "│"), paint(DIM, placeholder)));
142 } else {
143 for line in &self.buf {
144 f.push(format!(
145 "{} {}",
146 paint(DIM, "│"),
147 paint(super::settings::colors().input, line)
148 ));
149 }
150 }
151 if self.show_submit {
152 let c = super::settings::colors();
153 let style = if self.focus_submit { c.accent } else { c.dim };
154 f.push(format!(
155 "{} {}",
156 paint(DIM, "│"),
157 paint(style, "[ submit ]")
158 ));
159 }
160 f.push(theme::hint(if self.show_submit {
161 "Tab to focus submit · Enter to insert newline"
162 } else {
163 "Enter for newline · Enter on empty line to submit"
164 }));
165 f.push(theme::frame_bot(None));
166 f
167 }
168
169 fn render_answered(&self, value: &String) -> Frame {
170 let snippet: String = value
171 .lines()
172 .next()
173 .unwrap_or("")
174 .chars()
175 .take(40)
176 .collect();
177 let suffix = if value.lines().count() > 1 {
178 " …"
179 } else {
180 ""
181 };
182 theme::answered(&self.label, &format!("{snippet}{suffix}"))
183 .split("\r\n")
184 .map(String::from)
185 .collect()
186 }
187
188 fn run_fallback(self) -> Result<String> {
189 use std::io::{BufRead, Write};
190 let mut err = std::io::stderr();
191 writeln!(err, " {} (end with empty line)", self.label).map_err(PromptError::Io)?;
192 err.flush().map_err(PromptError::Io)?;
193 let stdin = std::io::stdin();
194 let mut acc = String::new();
195 for line in stdin.lock().lines() {
196 let l = line.map_err(PromptError::Io)?;
197 if l.is_empty() {
198 break;
199 }
200 if !acc.is_empty() {
201 acc.push('\n');
202 }
203 acc.push_str(&l);
204 }
205 Ok(acc)
206 }
207}