A simple TUI Library written in Rust
at main 159 lines 5.2 kB view raw
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}