zeroclaw_runtime/onboard/ui/
quick.rs1use std::collections::HashMap;
10
11use anyhow::{Result, bail};
12use async_trait::async_trait;
13use zeroclaw_config::traits::{Answer, OnboardUi, SelectItem};
14
15#[derive(Debug, Default)]
16pub struct QuickUi {
17 answers: HashMap<String, String>,
18 sequences: HashMap<String, Vec<String>>,
23 sequence_cursor: HashMap<String, usize>,
24}
25
26impl QuickUi {
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn with(mut self, prompt: impl Into<String>, value: impl Into<String>) -> Self {
32 self.answers.insert(prompt.into(), value.into());
33 self
34 }
35
36 pub fn with_sequence<I, S>(mut self, prompt: impl Into<String>, values: I) -> Self
41 where
42 I: IntoIterator<Item = S>,
43 S: Into<String>,
44 {
45 self.sequences
46 .insert(prompt.into(), values.into_iter().map(Into::into).collect());
47 self
48 }
49
50 fn lookup(&mut self, prompt: &str) -> Option<String> {
53 if let Some(seq) = self.sequences.get(prompt) {
54 let cursor = self.sequence_cursor.entry(prompt.to_string()).or_insert(0);
55 if let Some(v) = seq.get(*cursor) {
56 *cursor += 1;
57 return Some(v.clone());
58 }
59 }
60 self.answers.get(prompt).cloned()
61 }
62}
63
64#[async_trait]
65impl OnboardUi for QuickUi {
66 async fn confirm(&mut self, prompt: &str, default: bool) -> Result<Answer<bool>> {
67 Ok(Answer::Value(match self.lookup(prompt) {
68 Some(value) => matches!(
69 value.trim().to_ascii_lowercase().as_str(),
70 "true" | "yes" | "y" | "1"
71 ),
72 None => default,
73 }))
74 }
75
76 async fn string(
77 &mut self,
78 prompt: &str,
79 current: Option<&str>,
80 placeholder: Option<&str>,
81 ) -> Result<Answer<String>> {
82 if let Some(answer) = self.lookup(prompt) {
83 return Ok(Answer::Value(answer));
84 }
85 if let Some(value) = current {
86 return Ok(Answer::Value(value.to_string()));
87 }
88 if let Some(value) = placeholder {
91 return Ok(Answer::Value(value.to_string()));
92 }
93 bail!("quick mode: no answer or default provided for prompt {prompt:?}");
94 }
95
96 async fn secret(&mut self, prompt: &str, has_current: bool) -> Result<Answer<Option<String>>> {
97 match (self.lookup(prompt), has_current) {
98 (Some(value), _) => Ok(Answer::Value(Some(value))),
99 (None, true) => Ok(Answer::Value(None)),
100 (None, false) => {
101 bail!("quick mode: secret {prompt:?} is required but no value was supplied")
102 }
103 }
104 }
105
106 async fn select(
107 &mut self,
108 prompt: &str,
109 items: &[SelectItem],
110 current: Option<usize>,
111 ) -> Result<Answer<usize>> {
112 if let Some(answer) = self.lookup(prompt) {
113 if let Some(index) = items
114 .iter()
115 .position(|item| item.label.eq_ignore_ascii_case(&answer))
116 {
117 return Ok(Answer::Value(index));
118 }
119 bail!("quick mode: {prompt:?} answer {answer:?} matches none of the available options");
120 }
121 if let Some(index) = current {
122 return Ok(Answer::Value(index));
123 }
124 bail!("quick mode: no answer or default provided for prompt {prompt:?}");
125 }
126
127 async fn editor(&mut self, _hint: &str, initial: &str) -> Result<Answer<String>> {
128 Ok(Answer::Value(initial.to_string()))
129 }
130
131 fn heading(&mut self, level: u8, text: &str) {
132 let marker = "#".repeat(level.clamp(1, 6) as usize);
133 println!("\n{marker} {text}");
134 }
135
136 fn note(&mut self, msg: &str) {
137 println!("\n{msg}\n");
138 }
139
140 fn status(&mut self, msg: &str) {
141 println!(" {msg}");
142 }
143
144 fn warn(&mut self, msg: &str) {
145 eprintln!("⚠️ {msg}");
146 }
147}