outrig_cli/init/prompt/
dialoguer.rs1use std::io;
12
13use dialoguer::{FuzzySelect, Input, Select};
14use tokio::io::AsyncWriteExt;
15use tokio::task;
16
17use crate::error::{OutrigError, Result};
18use crate::init::prompt::{self, Field, PromptSource};
19
20#[derive(Debug, Default)]
21pub struct DialoguerPrompt;
22
23impl DialoguerPrompt {
24 pub fn new() -> Self {
25 Self
26 }
27}
28
29async fn write_description(description: &str) -> Result<()> {
34 if description.is_empty() {
35 return Ok(());
36 }
37 let line = format!("\n {description}\n");
38 let mut stderr = tokio::io::stderr();
39 stderr.write_all(line.as_bytes()).await?;
40 stderr.flush().await?;
41 Ok(())
42}
43
44async fn write_field_help(field: &Field) -> Result<()> {
45 let buf = prompt::format_field_help(field);
46 let mut stderr = tokio::io::stderr();
47 stderr.write_all(buf.as_bytes()).await?;
48 stderr.flush().await?;
49 Ok(())
50}
51
52async fn write_error(msg: &str) -> Result<()> {
53 let line = format!("[outrig] {msg}\n");
54 let mut stderr = tokio::io::stderr();
55 stderr.write_all(line.as_bytes()).await?;
56 stderr.flush().await?;
57 Ok(())
58}
59
60fn map_dialoguer_err(e: dialoguer::Error) -> OutrigError {
61 match e {
62 dialoguer::Error::IO(io_err) => OutrigError::Io(io_err),
63 }
64}
65
66fn map_join_err(e: task::JoinError) -> OutrigError {
67 OutrigError::Io(io::Error::other(e))
68}
69
70impl PromptSource for DialoguerPrompt {
71 async fn ask_string(&mut self, field: &Field, default: &str) -> Result<String> {
72 loop {
73 let prompt = field.name.to_owned();
74 let default_owned = default.to_owned();
75 let answer = task::spawn_blocking(move || {
76 Input::<String>::new()
77 .with_prompt(prompt)
78 .default(default_owned)
79 .allow_empty(true)
83 .interact_text()
84 })
85 .await
86 .map_err(map_join_err)?
87 .map_err(map_dialoguer_err)?;
88 if answer.trim() == "?" {
89 write_field_help(field).await?;
90 continue;
91 }
92 return Ok(answer);
93 }
94 }
95
96 async fn ask_bool(&mut self, field: &Field, default: bool) -> Result<bool> {
100 let render = if default { "Y/n" } else { "y/N" };
101 loop {
102 let prompt = format!("{} [{}]", field.name, render);
103 let answer = task::spawn_blocking(move || {
104 Input::<String>::new()
105 .with_prompt(prompt)
106 .allow_empty(true)
107 .interact_text()
108 })
109 .await
110 .map_err(map_join_err)?
111 .map_err(map_dialoguer_err)?;
112 let trimmed = answer.trim();
113 if trimmed.is_empty() {
114 return Ok(default);
115 }
116 if trimmed == "?" {
117 write_field_help(field).await?;
118 continue;
119 }
120 match prompt::parse_bool(trimmed) {
121 Some(b) => return Ok(b),
122 None => write_error("expected y/yes or n/no, or `?` for help").await?,
123 }
124 }
125 }
126
127 async fn ask_select(&mut self, field: &Field, default_idx: usize) -> Result<usize> {
128 write_description(field.description).await?;
129 let prompt = field.name.to_owned();
130 let items: Vec<&'static str> = field.options.iter().map(|(v, _)| *v).collect();
131 task::spawn_blocking(move || {
132 FuzzySelect::new()
133 .with_prompt(prompt)
134 .items(&items)
135 .default(default_idx)
136 .interact()
137 })
138 .await
139 .map_err(map_join_err)?
140 .map_err(map_dialoguer_err)
141 .map_err(Into::into)
142 }
143
144 async fn ask_multiselect(
151 &mut self,
152 field: &Field,
153 default_indices: &[usize],
154 ) -> Result<Vec<usize>> {
155 write_description(field.description).await?;
156 let mut selected: Vec<bool> = vec![false; field.options.len()];
157 for &i in default_indices {
158 if let Some(slot) = selected.get_mut(i) {
159 *slot = true;
160 }
161 }
162 let done_idx = field.options.len();
163 let mut cursor: usize = 0;
164 loop {
165 let mut items: Vec<String> = field
166 .options
167 .iter()
168 .enumerate()
169 .map(|(i, (val, _))| {
170 let mark = if selected[i] { "[x]" } else { "[ ]" };
171 format!("{mark} {val}")
172 })
173 .collect();
174 items.push("Done".to_string());
175
176 let prompt = field.name.to_owned();
177 let cursor_now = cursor.min(items.len() - 1);
178 let idx = task::spawn_blocking(move || {
179 Select::new()
180 .with_prompt(prompt)
181 .items(&items)
182 .default(cursor_now)
183 .report(false)
187 .interact()
188 })
189 .await
190 .map_err(map_join_err)?
191 .map_err(map_dialoguer_err)?;
192
193 if idx == done_idx {
194 return Ok(selected
195 .iter()
196 .enumerate()
197 .filter_map(|(i, &b)| if b { Some(i) } else { None })
198 .collect());
199 }
200 selected[idx] = !selected[idx];
201 cursor = idx;
202 }
203 }
204}