A simple TUI Library written in Rust
1//! API key setup — provider selector, key validation, timeout, then verification.
2
3use std::thread;
4use std::time::Duration;
5
6use sly::ansi;
7use sly::block::Block;
8use sly::color::Color;
9use sly::component::RunError;
10use sly::flow::Flow;
11use sly::input;
12use sly::style::{Span, Style};
13use sly::terminal;
14use sly::view::View;
15use sly::widgets::{
16 checklist::{CheckState, Checklist, ChecklistItem},
17 grouped_selector::{self, GroupedItem, GroupedSelector},
18 number_input::{self, NumberInput},
19 selector::SelectorItem,
20 spinner,
21 text_input::{self, TextInput},
22};
23
24fn validate_api_key(s: &str) -> Result<(), String> {
25 let t = s.trim();
26 if t.is_empty() {
27 return Err("API key is required".into());
28 }
29 if t.len() < 20 {
30 return Err("API key must be at least 20 characters".into());
31 }
32 Ok(())
33}
34
35fn main() {
36 println!("SLY — API Key Setup");
37 println!("Up/Down + Enter to choose, Escape to cancel");
38 println!();
39
40 let providers: Vec<GroupedItem<&'static str>> = vec![
41 GroupedItem::Header("AI Providers".to_string()),
42 GroupedItem::Item(SelectorItem::new("OpenAI", "openai")),
43 GroupedItem::Item(SelectorItem::new("Anthropic", "anthropic")),
44 GroupedItem::Item(SelectorItem::new("Mistral", "mistral")),
45 GroupedItem::Separator,
46 GroupedItem::Header("Cloud".to_string()),
47 GroupedItem::Item(SelectorItem::new("AWS", "aws")),
48 GroupedItem::Item(SelectorItem::new("Google Cloud", "gcp")),
49 GroupedItem::Item(SelectorItem::new("Azure", "azure")),
50 ];
51
52 let flow: Flow<Option<(&'static str, String, f64)>> = Flow::step(
53 grouped_selector::grouped_selector(GroupedSelector::new(providers).max_height(7)),
54 |provider: &'static str| {
55 Flow::step(
56 text_input::text_input(
57 TextInput::new()
58 .prompt("API Key: ")
59 .placeholder("Paste your key…")
60 .validator(validate_api_key)
61 .display_width(42),
62 ),
63 move |key: String| {
64 Flow::step(
65 number_input::number_input(
66 NumberInput::new(30.0)
67 .prompt("Timeout (s): ")
68 .min(1.0)
69 .max(300.0)
70 .integer(),
71 ),
72 move |timeout: f64| Flow::done(Some((provider, key, timeout))),
73 )
74 },
75 )
76 },
77 );
78
79 match input::with_raw_mode(|mode| flow.run(mode)) {
80 Ok(Ok(Some((provider, key, timeout)))) => {
81 println!();
82 verify_api_key(provider, &key, timeout as u64);
83 }
84 Ok(Ok(None)) => {}
85 Ok(Err(RunError::Cancelled)) => println!("{}", styled_warn("Cancelled")),
86 Ok(Err(RunError::InputError(e))) => println!("Input error: {e}"),
87 Err(e) => println!("Raw mode error: {e}"),
88 }
89}
90
91fn verify_api_key(provider: &str, key: &str, _timeout_s: u64) {
92 let width = terminal::get_size()
93 .map(|(c, _)| c as usize)
94 .unwrap_or(80)
95 .min(60);
96
97 let masked = if key.len() <= 8 {
98 "••••••••".to_string()
99 } else {
100 format!("{}…{}", &key[..6], &key[key.len().saturating_sub(4)..])
101 };
102
103 println!(" Provider : {provider}");
104 println!(" Key : {masked}");
105 println!(" Timeout : {_timeout_s}s");
106 println!();
107
108 let mut checklist = Checklist::new(vec![
109 ChecklistItem::new("Validate key format"),
110 ChecklistItem::new("Connect to endpoint"),
111 ChecklistItem::new("Check permissions"),
112 ChecklistItem::new("Cache credentials"),
113 ]);
114
115 let spin = spinner::Spinner::braille().label("Verifying…");
116 let mut view = View::create(
117 &Block::vstack(vec![spin.frame(0), checklist.to_block()], 1),
118 width,
119 );
120
121 let steps: &[(usize, &str, u64)] = &[
122 (0, "valid", 200),
123 (1, "connected", 500),
124 (2, "authorized", 400),
125 (3, "cached", 250),
126 ];
127
128 let mut tick: usize = 0;
129 for &(idx, note, ms) in steps {
130 checklist.set_state(idx, CheckState::InProgress);
131 for _ in 0..4 {
132 thread::sleep(Duration::from_millis(ms / 4));
133 view.update(&Block::vstack(
134 vec![spin.frame(tick), checklist.to_block()],
135 1,
136 ));
137 tick += 1;
138 }
139 checklist.set_state(idx, CheckState::Done);
140 checklist.set_note(idx, Some(note.into()));
141 view.update(&Block::vstack(
142 vec![spin.frame(tick), checklist.to_block()],
143 1,
144 ));
145 tick += 1;
146 }
147
148 let ok = Block::text(vec![vec![Span::styled(
149 format!("✓ {provider} configured successfully"),
150 Style::new().bold().fg(Color::Green),
151 )]]);
152 view.update(&Block::vstack(vec![ok, checklist.to_block()], 1));
153 view.finish();
154 println!();
155}
156
157fn styled_warn(msg: &str) -> String {
158 ansi::styled(msg, &[ansi::fg(&Color::Yellow)])
159}