zeroclaw_runtime/onboard/ui/
term.rs1use anyhow::Result;
9use async_trait::async_trait;
10use dialoguer::{Confirm, Editor, FuzzySelect, Input, Password};
11use zeroclaw_config::traits::{Answer, OnboardUi, SelectItem};
12
13pub struct TermUi;
14
15#[async_trait]
16impl OnboardUi for TermUi {
17 async fn confirm(&mut self, prompt: &str, default: bool) -> Result<Answer<bool>> {
18 let prompt = prompt.to_string();
19 tokio::task::spawn_blocking(move || -> Result<Answer<bool>> {
20 let v = Confirm::new()
21 .with_prompt(prompt)
22 .default(default)
23 .interact_opt()?;
24 Ok(match v {
25 Some(b) => Answer::Value(b),
26 None => Answer::Back,
27 })
28 })
29 .await?
30 }
31
32 async fn string(
33 &mut self,
34 prompt: &str,
35 current: Option<&str>,
36 placeholder: Option<&str>,
37 ) -> Result<Answer<String>> {
38 let prompt = prompt.to_string();
43 let default = current
49 .map(ToOwned::to_owned)
50 .or_else(|| placeholder.map(ToOwned::to_owned));
51 tokio::task::spawn_blocking(move || -> Result<Answer<String>> {
52 let mut input = Input::<String>::new().with_prompt(prompt).allow_empty(true);
53 if let Some(value) = default {
54 input = input.default(value);
55 }
56 Ok(Answer::Value(input.interact_text()?))
57 })
58 .await?
59 }
60
61 async fn secret(&mut self, prompt: &str, has_current: bool) -> Result<Answer<Option<String>>> {
62 let prompt = prompt.to_string();
63 tokio::task::spawn_blocking(move || -> Result<Answer<Option<String>>> {
64 if has_current {
65 let replace = Confirm::new()
66 .with_prompt(format!("{prompt} (stored, replace?)"))
67 .default(false)
68 .interact_opt()?;
69 match replace {
70 Some(false) => return Ok(Answer::Value(None)),
71 None => return Ok(Answer::Back),
72 Some(true) => {}
73 }
74 }
75 let value = Password::new().with_prompt(prompt).interact()?;
76 Ok(Answer::Value(Some(value)))
77 })
78 .await?
79 }
80
81 async fn select(
82 &mut self,
83 prompt: &str,
84 items: &[SelectItem],
85 current: Option<usize>,
86 ) -> Result<Answer<usize>> {
87 let prompt = prompt.to_string();
88 let labels: Vec<String> = items
89 .iter()
90 .map(|item| match &item.badge {
91 Some(badge) => format!("{} {badge}", item.label),
92 None => item.label.clone(),
93 })
94 .collect();
95 tokio::task::spawn_blocking(move || -> Result<Answer<usize>> {
96 let mut select = FuzzySelect::new().with_prompt(prompt).items(&labels);
97 if let Some(index) = current {
98 select = select.default(index);
99 }
100 Ok(match select.interact_opt()? {
101 Some(i) => Answer::Value(i),
102 None => Answer::Back,
103 })
104 })
105 .await?
106 }
107
108 async fn editor(&mut self, hint: &str, initial: &str) -> Result<Answer<String>> {
109 let hint = hint.to_string();
110 let buffer = initial.to_string();
111 tokio::task::spawn_blocking(move || -> Result<Answer<String>> {
112 if !hint.is_empty() {
113 println!(" {hint}");
114 }
115 match Editor::new().edit(&buffer)? {
119 Some(edited) => Ok(Answer::Value(edited)),
120 None => Ok(Answer::Back),
121 }
122 })
123 .await?
124 }
125
126 fn heading(&mut self, level: u8, text: &str) {
127 let marker = "#".repeat(level.clamp(1, 6) as usize);
131 println!("\n{marker} {text}");
132 if level == 1 {
133 let rule_width = text.chars().count().saturating_add(2).max(20);
134 println!("{}", "─".repeat(rule_width));
135 }
136 }
137
138 fn note(&mut self, msg: &str) {
139 println!("\n{msg}\n");
140 }
141
142 fn status(&mut self, msg: &str) {
143 println!(" {msg}");
144 }
145
146 fn warn(&mut self, msg: &str) {
147 eprintln!("⚠️ {msg}");
148 }
149}