1use anyhow::{anyhow, Result};
4use crossterm::event::{
5 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
6};
7use crossterm::execute;
8use crossterm::terminal::{
9 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
10};
11use ratatui::backend::CrosstermBackend;
12use ratatui::layout::{Constraint, Direction, Layout};
13use ratatui::style::{Color, Modifier, Style};
14use ratatui::text::{Line, Span, Text};
15use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
16use ratatui::Terminal;
17use std::io::stdout;
18
19use crate::client::RommClient;
20use crate::config::{
21 is_keyring_placeholder, load_config, normalize_romm_origin, persist_user_config,
22 read_user_config_json_from_disk, AuthConfig, Config,
23};
24
25#[derive(Clone, Copy, PartialEq, Eq)]
26enum AuthKind {
27 None,
28 Basic,
29 Bearer,
30 ApiKey,
31}
32
33#[derive(Clone, Copy, PartialEq, Eq)]
34enum Step {
35 Url,
36 Https,
37 Download,
38 AuthMenu,
39 BasicUser,
40 BasicPass,
41 Bearer,
42 ApiHeader,
43 ApiKey,
44 Summary,
45}
46
47pub struct SetupWizard {
49 step: Step,
50 auth_kind: AuthKind,
51 auth_menu_selected: usize,
52 url: String,
53 url_cursor: usize,
54 download_dir: String,
55 dl_cursor: usize,
56 username: String,
57 user_cursor: usize,
58 password: String,
59 bearer_token: String,
60 bearer_cursor: usize,
61 api_header: String,
62 header_cursor: usize,
63 api_key: String,
64 api_key_cursor: usize,
65 reuse_keyring_password: bool,
67 reuse_keyring_bearer: bool,
68 reuse_keyring_api_key: bool,
69 pub testing: bool,
70 pub use_https: bool,
71 pub error: Option<String>,
72}
73
74impl SetupWizard {
75 pub fn new() -> Self {
76 let default_dl = dirs::download_dir()
77 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join("Downloads"))
78 .join("romm-cli")
79 .display()
80 .to_string();
81 Self {
82 step: Step::Url,
83 auth_kind: AuthKind::None,
84 auth_menu_selected: 0,
85 url: "https://".to_string(),
86 url_cursor: "https://".len(),
87 download_dir: default_dl,
88 dl_cursor: 0,
89 username: String::new(),
90 user_cursor: 0,
91 password: String::new(),
92 bearer_token: String::new(),
93 bearer_cursor: 0,
94 api_header: String::new(),
95 header_cursor: 0,
96 api_key: String::new(),
97 api_key_cursor: 0,
98 reuse_keyring_password: false,
99 reuse_keyring_bearer: false,
100 reuse_keyring_api_key: false,
101 testing: false,
102 use_https: true,
103 error: None,
104 }
105 }
106
107 pub fn new_auth_only(config: &Config) -> Self {
108 let mut wizard = Self::new();
109 wizard.step = Step::AuthMenu;
110 wizard.url = config.base_url.clone();
111 wizard.download_dir = config.download_dir.clone();
112 wizard.use_https = config.use_https;
113
114 let disk = read_user_config_json_from_disk();
115
116 match &config.auth {
117 Some(AuthConfig::Basic { username, password }) => {
118 wizard.auth_kind = AuthKind::Basic;
119 wizard.auth_menu_selected = 1;
120 wizard.username = username.clone();
121 wizard.user_cursor = username.len();
122 let disk_pass = disk
123 .as_ref()
124 .and_then(|c| c.auth.as_ref())
125 .and_then(|a| match a {
126 AuthConfig::Basic { password, .. } => Some(password.as_str()),
127 _ => None,
128 });
129 if disk_pass.is_some_and(is_keyring_placeholder) {
130 wizard.password = String::new();
131 wizard.reuse_keyring_password = true;
132 } else {
133 wizard.password = password.clone();
134 }
135 }
136 Some(AuthConfig::Bearer { token }) => {
137 wizard.auth_kind = AuthKind::Bearer;
138 wizard.auth_menu_selected = 2;
139 let disk_tok = disk
140 .as_ref()
141 .and_then(|c| c.auth.as_ref())
142 .and_then(|a| match a {
143 AuthConfig::Bearer { token } => Some(token.as_str()),
144 _ => None,
145 });
146 if disk_tok.is_some_and(is_keyring_placeholder) {
147 wizard.bearer_token = String::new();
148 wizard.bearer_cursor = 0;
149 wizard.reuse_keyring_bearer = true;
150 } else {
151 wizard.bearer_token = token.clone();
152 wizard.bearer_cursor = token.len();
153 }
154 }
155 Some(AuthConfig::ApiKey { header, key }) => {
156 wizard.auth_kind = AuthKind::ApiKey;
157 wizard.auth_menu_selected = 3;
158 wizard.api_header = header.clone();
159 wizard.header_cursor = header.len();
160 let disk_key = disk
161 .as_ref()
162 .and_then(|c| c.auth.as_ref())
163 .and_then(|a| match a {
164 AuthConfig::ApiKey { key, .. } => Some(key.as_str()),
165 _ => None,
166 });
167 if disk_key.is_some_and(is_keyring_placeholder) {
168 wizard.api_key = String::new();
169 wizard.api_key_cursor = 0;
170 wizard.reuse_keyring_api_key = true;
171 } else {
172 wizard.api_key = key.clone();
173 wizard.api_key_cursor = key.len();
174 }
175 }
176 None => {
177 wizard.auth_kind = AuthKind::None;
178 wizard.auth_menu_selected = 0;
179 }
180 }
181 wizard
182 }
183
184 fn auth_labels() -> [&'static str; 4] {
185 [
186 "No authentication",
187 "Basic (username + password)",
188 "API Token (Bearer)",
189 "API key in custom header",
190 ]
191 }
192
193 fn auth_kind_from_index(i: usize) -> AuthKind {
194 match i {
195 1 => AuthKind::Basic,
196 2 => AuthKind::Bearer,
197 3 => AuthKind::ApiKey,
198 _ => AuthKind::None,
199 }
200 }
201
202 fn build_config(&self) -> Result<Config> {
203 let base_url = normalize_romm_origin(self.url.trim());
204 if base_url.is_empty() {
205 return Err(anyhow!("Server URL cannot be empty"));
206 }
207 let auth: Option<AuthConfig> = match self.auth_kind {
208 AuthKind::None => None,
209 AuthKind::Basic => {
210 let u = self.username.trim();
211 if u.is_empty() {
212 return Err(anyhow!("Username cannot be empty"));
213 }
214 let password = if self.password.is_empty() && self.reuse_keyring_password {
215 crate::config::keyring_get("API_PASSWORD").ok_or_else(|| {
216 anyhow!("Password not in OS keyring; enter a password or run romm-cli init")
217 })?
218 } else if self.password.is_empty() {
219 return Err(anyhow!("Password cannot be empty"));
220 } else {
221 self.password.clone()
222 };
223 Some(AuthConfig::Basic {
224 username: u.to_string(),
225 password,
226 })
227 }
228 AuthKind::Bearer => {
229 let token = if self.bearer_token.trim().is_empty() && self.reuse_keyring_bearer {
230 crate::config::keyring_get("API_TOKEN").ok_or_else(|| {
231 anyhow!("API token not in OS keyring; enter a token or run romm-cli init")
232 })?
233 } else if self.bearer_token.trim().is_empty() {
234 return Err(anyhow!("Bearer token cannot be empty"));
235 } else {
236 self.bearer_token.trim().to_string()
237 };
238 Some(AuthConfig::Bearer { token })
239 }
240 AuthKind::ApiKey => {
241 let h = self.api_header.trim();
242 if h.is_empty() {
243 return Err(anyhow!("Header name cannot be empty"));
244 }
245 let key = if self.api_key.is_empty() && self.reuse_keyring_api_key {
246 crate::config::keyring_get("API_KEY").ok_or_else(|| {
247 anyhow!("API key not in OS keyring; enter a key or run romm-cli init")
248 })?
249 } else if self.api_key.is_empty() {
250 return Err(anyhow!("API key cannot be empty"));
251 } else {
252 self.api_key.clone()
253 };
254 Some(AuthConfig::ApiKey {
255 header: h.to_string(),
256 key,
257 })
258 }
259 };
260 Ok(Config {
261 base_url,
262 download_dir: self.download_dir.trim().to_string(),
263 use_https: self.use_https,
264 auth,
265 })
266 }
267
268 pub fn render(&mut self, f: &mut ratatui::Frame, area: ratatui::layout::Rect) {
269 let title = match self.step {
270 Step::Url => "Step 1/5 — RomM server URL",
271 Step::Https => "Step 2/5 — Secure connection",
272 Step::Download => "Step 3/5 — Download directory",
273 Step::AuthMenu => "Step 4/5 — Authentication",
274 Step::BasicUser | Step::BasicPass => "Step 5/5 — Basic auth",
275 Step::Bearer => "Step 5/5 — API Token",
276 Step::ApiHeader | Step::ApiKey => "Step 5/5 — API key",
277 Step::Summary => "Review & connect",
278 };
279
280 let main = Layout::default()
281 .direction(Direction::Vertical)
282 .constraints([
283 Constraint::Length(3),
284 Constraint::Min(6),
285 Constraint::Length(4),
286 ])
287 .split(area);
288
289 let hint_top = "Same origin as in your browser (no trailing /api). Esc: quit";
290 let p = Paragraph::new(hint_top).style(Style::default().fg(Color::DarkGray));
291 f.render_widget(p, main[0]);
292
293 match self.step {
294 Step::Url => {
295 let line = format!(
296 "{}▏",
297 self.url.chars().take(self.url_cursor).collect::<String>()
298 );
299 let rest: String = self.url.chars().skip(self.url_cursor).collect();
300 let text = format!("{line}{rest}");
301 let block = Block::default().title(title).borders(Borders::ALL);
302 let p = Paragraph::new(text).block(block);
303 f.render_widget(p, main[1]);
304 }
305 Step::Https => {
306 let text = if self.use_https {
307 "[X] Use HTTPS (Recommended)"
308 } else {
309 "[ ] Use HTTPS (Insecure)"
310 };
311 let block = Block::default().title(title).borders(Borders::ALL);
312 let p = Paragraph::new(format!("\n {}\n\n Space: toggle Enter: next", text))
313 .block(block);
314 f.render_widget(p, main[1]);
315 }
316 Step::Download => {
317 let line = format!(
318 "{}▏",
319 self.download_dir
320 .chars()
321 .take(self.dl_cursor)
322 .collect::<String>()
323 );
324 let rest: String = self.download_dir.chars().skip(self.dl_cursor).collect();
325 let text = format!("{line}{rest}");
326 let block = Block::default().title(title).borders(Borders::ALL);
327 let p = Paragraph::new(text).block(block);
328 f.render_widget(p, main[1]);
329 }
330 Step::AuthMenu => {
331 let items: Vec<ListItem> = Self::auth_labels()
332 .iter()
333 .map(|s| ListItem::new(*s))
334 .collect();
335 let mut state = ListState::default();
336 state.select(Some(self.auth_menu_selected));
337 let list = List::new(items)
338 .block(Block::default().title(title).borders(Borders::ALL))
339 .highlight_style(
340 Style::default()
341 .fg(Color::Yellow)
342 .add_modifier(Modifier::BOLD),
343 )
344 .highlight_symbol(">> ");
345 f.render_stateful_widget(list, main[1], &mut state);
346 }
347 Step::BasicUser | Step::BasicPass => {
348 let user_line = if self.step == Step::BasicUser {
349 format!(
350 "{}▏{}",
351 self.username
352 .chars()
353 .take(self.user_cursor)
354 .collect::<String>(),
355 self.username
356 .chars()
357 .skip(self.user_cursor)
358 .collect::<String>()
359 )
360 } else {
361 self.username.clone()
362 };
363 let pass_display: String = if self.step == Step::BasicPass {
364 "•".repeat(self.password.len()) + "▏"
365 } else {
366 "•".repeat(self.password.len())
367 };
368 let kr_hint = if self.step == Step::BasicPass
369 && self.password.is_empty()
370 && self.reuse_keyring_password
371 {
372 "\n\n(stored in OS keyring — leave blank to keep, or type a new password)"
373 } else {
374 ""
375 };
376 let block = Block::default().title(title).borders(Borders::ALL);
377 let body = format!(
378 "Username\n{user_line}\n\nPassword (hidden)\n{pass_display}{kr_hint}\n\nTab: switch field"
379 );
380 let p = Paragraph::new(body).block(block);
381 f.render_widget(p, main[1]);
382 }
383 Step::Bearer => {
384 let line = format!(
385 "{}▏{}",
386 self.bearer_token
387 .chars()
388 .take(self.bearer_cursor)
389 .collect::<String>(),
390 self.bearer_token
391 .chars()
392 .skip(self.bearer_cursor)
393 .collect::<String>()
394 );
395 let mut bearer_text = Text::from(vec![Line::from(line)]);
396 if self.bearer_token.is_empty() && self.reuse_keyring_bearer {
397 bearer_text.push_line(Line::from(""));
398 bearer_text.push_line(Line::from(Span::styled(
399 "Token stored in OS keyring — leave blank to keep, or type a new token.",
400 Style::default().fg(Color::DarkGray),
401 )));
402 }
403 let block = Block::default().title(title).borders(Borders::ALL);
404 let p = Paragraph::new(bearer_text).block(block);
405 f.render_widget(p, main[1]);
406 }
407 Step::ApiHeader | Step::ApiKey => {
408 let header_line = if self.step == Step::ApiHeader {
409 format!(
410 "{}▏{}",
411 self.api_header
412 .chars()
413 .take(self.header_cursor)
414 .collect::<String>(),
415 self.api_header
416 .chars()
417 .skip(self.header_cursor)
418 .collect::<String>()
419 )
420 } else {
421 self.api_header.clone()
422 };
423 let key_line = if self.step == Step::ApiKey {
424 "•".repeat(self.api_key.len()) + "▏"
425 } else {
426 "•".repeat(self.api_key.len())
427 };
428 let kr_hint = if self.step == Step::ApiKey
429 && self.api_key.is_empty()
430 && self.reuse_keyring_api_key
431 {
432 "\n\n(stored in OS keyring — leave blank to keep, or type a new key)"
433 } else {
434 ""
435 };
436 let body = format!(
437 "Header name\n{header_line}\n\nKey (hidden)\n{key_line}{kr_hint}\n\nTab: switch field"
438 );
439 let block = Block::default().title(title).borders(Borders::ALL);
440 let p = Paragraph::new(body).block(block);
441 f.render_widget(p, main[1]);
442 }
443 Step::Summary => {
444 let url_line = normalize_romm_origin(self.url.trim());
445 let auth_desc = match self.auth_kind {
446 AuthKind::None => "None",
447 AuthKind::Basic => "Basic",
448 AuthKind::Bearer => "API Token",
449 AuthKind::ApiKey => "API key header",
450 };
451 let mut lines = vec![
452 format!("Server: {url_line}"),
453 format!("Downloads: {}", self.download_dir.trim()),
454 format!("Use HTTPS: {}", if self.use_https { "Yes" } else { "No" }),
455 format!("Auth: {auth_desc}"),
456 String::new(),
457 ];
458 if self.testing {
459 lines.push("Connecting to server…".to_string());
460 } else if let Some(ref e) = self.error {
461 lines.push(format!("Last error: {e}"));
462 } else {
463 lines.push("Enter: test connection and save Esc: quit".to_string());
464 }
465 let block = Block::default().title(title).borders(Borders::ALL);
466 let p = Paragraph::new(lines.join("\n")).block(block);
467 f.render_widget(p, main[1]);
468 }
469 }
470
471 let footer = match self.step {
472 Step::Url => "Enter: next Backspace: delete Esc: quit",
473 Step::Https => "Space: toggle Enter: next Esc: quit",
474 Step::Download => "Enter: next Backspace: delete Esc: quit",
475 Step::AuthMenu => "↑/↓: choose Enter: next Esc: quit",
476 Step::BasicUser | Step::BasicPass => {
477 "Type text Tab: switch field Enter: next step Esc: quit"
478 }
479 Step::Bearer => "Enter: next step Esc: quit",
480 Step::ApiHeader | Step::ApiKey => "Tab: switch field Enter: next step Esc: quit",
481 Step::Summary => {
482 if self.testing {
483 "Please wait…"
484 } else {
485 "Enter: connect & save"
486 }
487 }
488 };
489 let p = Paragraph::new(footer)
490 .style(Style::default().fg(Color::Cyan))
491 .block(Block::default().borders(Borders::ALL));
492 f.render_widget(p, main[2]);
493 }
494
495 pub fn cursor_pos(&self, area: ratatui::layout::Rect) -> Option<(u16, u16)> {
496 let main = Layout::default()
497 .direction(Direction::Vertical)
498 .constraints([
499 Constraint::Length(3),
500 Constraint::Min(6),
501 Constraint::Length(4),
502 ])
503 .split(area);
504 let inner = main[1];
505 match self.step {
506 Step::Url => {
507 let x = inner.x + 1 + self.url_cursor.min(self.url.len()) as u16;
508 Some((x, inner.y + 1))
509 }
510 Step::Download => {
511 let x = inner.x + 1 + self.dl_cursor.min(self.download_dir.len()) as u16;
512 Some((x, inner.y + 1))
513 }
514 Step::Bearer => {
515 let x = inner.x + 1 + self.bearer_cursor.min(self.bearer_token.len()) as u16;
516 Some((x, inner.y + 1))
517 }
518 Step::BasicUser => {
519 let x = inner.x + 1 + self.user_cursor.min(self.username.len()) as u16;
520 Some((x, inner.y + 2))
521 }
522 Step::BasicPass => {
523 let x = inner.x + 1 + "•".repeat(self.password.len()).len() as u16;
524 Some((x, inner.y + 6))
525 }
526 Step::ApiHeader => {
527 let x = inner.x + 1 + self.header_cursor.min(self.api_header.len()) as u16;
528 Some((x, inner.y + 2))
529 }
530 Step::ApiKey => {
531 let x = inner.x + 1 + self.api_key_cursor.min(self.api_key.len()) as u16;
532 Some((x, inner.y + 6))
533 }
534 Step::Https | Step::AuthMenu | Step::Summary => None,
535 }
536 }
537
538 fn add_char_url(&mut self, c: char) {
539 let pos = self.url_cursor.min(self.url.len());
540 self.url.insert(pos, c);
541 self.url_cursor = pos + 1;
542 }
543
544 fn del_char_url(&mut self) {
545 if self.url_cursor > 0 && self.url_cursor <= self.url.len() {
546 self.url.remove(self.url_cursor - 1);
547 self.url_cursor -= 1;
548 }
549 }
550
551 fn add_char_dl(&mut self, c: char) {
552 let pos = self.dl_cursor.min(self.download_dir.len());
553 self.download_dir.insert(pos, c);
554 self.dl_cursor = pos + 1;
555 }
556
557 fn del_char_dl(&mut self) {
558 if self.dl_cursor > 0 && self.dl_cursor <= self.download_dir.len() {
559 self.download_dir.remove(self.dl_cursor - 1);
560 self.dl_cursor -= 1;
561 }
562 }
563
564 fn advance_from_auth_menu(&mut self) {
565 self.auth_kind = Self::auth_kind_from_index(self.auth_menu_selected);
566 self.step = match self.auth_kind {
567 AuthKind::None => Step::Summary,
568 AuthKind::Basic => Step::BasicUser,
569 AuthKind::Bearer => Step::Bearer,
570 AuthKind::ApiKey => Step::ApiHeader,
571 };
572 }
573
574 fn advance_step(&mut self) -> Result<()> {
575 self.error = None;
576 match self.step {
577 Step::Url => {
578 if normalize_romm_origin(self.url.trim()).is_empty() {
579 self.error = Some("Enter a valid server URL".to_string());
580 return Ok(());
581 }
582 self.step = Step::Https;
583 }
584 Step::Https => {
585 self.step = Step::Download;
586 self.dl_cursor = self.download_dir.len();
587 }
588 Step::Download => {
589 if self.download_dir.trim().is_empty() {
590 self.error = Some("Download path cannot be empty".to_string());
591 return Ok(());
592 }
593 self.step = Step::AuthMenu;
594 }
595 Step::AuthMenu => self.advance_from_auth_menu(),
596 Step::BasicUser => self.step = Step::BasicPass,
597 Step::BasicPass => self.step = Step::Summary,
598 Step::Bearer => self.step = Step::Summary,
599 Step::ApiHeader => self.step = Step::ApiKey,
600 Step::ApiKey => self.step = Step::Summary,
601 Step::Summary => {}
602 }
603 Ok(())
604 }
605
606 pub async fn try_connect_and_persist(&mut self, verbose: bool) -> Result<Config> {
607 let cfg = self.build_config()?;
608 let client = RommClient::new(&cfg, verbose)?;
609 client.fetch_openapi_json().await?;
610 let base = cfg.base_url.clone();
611 let download = self.download_dir.trim().to_string();
612 persist_user_config(&base, &download, self.use_https, cfg.auth.clone())?;
613 load_config()
614 }
615
616 pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> Result<bool> {
617 if key.kind != KeyEventKind::Press {
618 return Ok(false);
619 }
620 if key.code == KeyCode::Esc {
621 return Ok(true); }
623
624 if self.testing {
625 return Ok(false);
626 }
627
628 match self.step {
629 Step::Url => match key.code {
630 KeyCode::Enter => {
631 let _ = self.advance_step();
632 }
633 KeyCode::Char(c) => self.add_char_url(c),
634 KeyCode::Backspace => self.del_char_url(),
635 KeyCode::Left => {
636 if self.url_cursor > 0 {
637 self.url_cursor -= 1;
638 }
639 }
640 KeyCode::Right => {
641 if self.url_cursor < self.url.len() {
642 self.url_cursor += 1;
643 }
644 }
645 _ => {}
646 },
647 Step::Https => match key.code {
648 KeyCode::Enter => {
649 let _ = self.advance_step();
650 }
651 KeyCode::Char(' ') => self.use_https = !self.use_https,
652 _ => {}
653 },
654 Step::Download => match key.code {
655 KeyCode::Enter => {
656 let _ = self.advance_step();
657 }
658 KeyCode::Char(c) => self.add_char_dl(c),
659 KeyCode::Backspace => self.del_char_dl(),
660 KeyCode::Left => {
661 if self.dl_cursor > 0 {
662 self.dl_cursor -= 1;
663 }
664 }
665 KeyCode::Right => {
666 if self.dl_cursor < self.download_dir.len() {
667 self.dl_cursor += 1;
668 }
669 }
670 _ => {}
671 },
672 Step::AuthMenu => match key.code {
673 KeyCode::Up | KeyCode::Char('k') => {
674 if self.auth_menu_selected > 0 {
675 self.auth_menu_selected -= 1;
676 }
677 }
678 KeyCode::Down | KeyCode::Char('j') => {
679 if self.auth_menu_selected < 3 {
680 self.auth_menu_selected += 1;
681 }
682 }
683 KeyCode::Enter => {
684 let _ = self.advance_step();
685 }
686 _ => {}
687 },
688 Step::BasicUser => match key.code {
689 KeyCode::Tab => self.step = Step::BasicPass,
690 KeyCode::Enter => {
691 let _ = self.advance_step();
692 }
693 KeyCode::Char(c) => {
694 let pos = self.user_cursor.min(self.username.len());
695 self.username.insert(pos, c);
696 self.user_cursor = pos + 1;
697 }
698 KeyCode::Backspace => {
699 if self.user_cursor > 0 && self.user_cursor <= self.username.len() {
700 self.username.remove(self.user_cursor - 1);
701 self.user_cursor -= 1;
702 }
703 }
704 KeyCode::Left => {
705 if self.user_cursor > 0 {
706 self.user_cursor -= 1;
707 }
708 }
709 KeyCode::Right => {
710 if self.user_cursor < self.username.len() {
711 self.user_cursor += 1;
712 }
713 }
714 _ => {}
715 },
716 Step::BasicPass => match key.code {
717 KeyCode::Tab => self.step = Step::BasicUser,
718 KeyCode::Enter => {
719 let _ = self.advance_step();
720 }
721 KeyCode::Char(c) => {
722 self.reuse_keyring_password = false;
723 self.password.push(c);
724 }
725 KeyCode::Backspace => {
726 self.password.pop();
727 }
728 _ => {}
729 },
730 Step::Bearer => match key.code {
731 KeyCode::Enter => {
732 let _ = self.advance_step();
733 }
734 KeyCode::Char(c) => {
735 self.reuse_keyring_bearer = false;
736 let pos = self.bearer_cursor.min(self.bearer_token.len());
737 self.bearer_token.insert(pos, c);
738 self.bearer_cursor = pos + 1;
739 }
740 KeyCode::Backspace => {
741 if self.bearer_cursor > 0 && self.bearer_cursor <= self.bearer_token.len() {
742 self.bearer_token.remove(self.bearer_cursor - 1);
743 self.bearer_cursor -= 1;
744 }
745 }
746 KeyCode::Left => {
747 if self.bearer_cursor > 0 {
748 self.bearer_cursor -= 1;
749 }
750 }
751 KeyCode::Right => {
752 if self.bearer_cursor < self.bearer_token.len() {
753 self.bearer_cursor += 1;
754 }
755 }
756 _ => {}
757 },
758 Step::ApiHeader => match key.code {
759 KeyCode::Tab => self.step = Step::ApiKey,
760 KeyCode::Enter => {
761 let _ = self.advance_step();
762 }
763 KeyCode::Char(c) => {
764 let pos = self.header_cursor.min(self.api_header.len());
765 self.api_header.insert(pos, c);
766 self.header_cursor = pos + 1;
767 }
768 KeyCode::Backspace => {
769 if self.header_cursor > 0 && self.header_cursor <= self.api_header.len() {
770 self.api_header.remove(self.header_cursor - 1);
771 self.header_cursor -= 1;
772 }
773 }
774 KeyCode::Left => {
775 if self.header_cursor > 0 {
776 self.header_cursor -= 1;
777 }
778 }
779 KeyCode::Right => {
780 if self.header_cursor < self.api_header.len() {
781 self.header_cursor += 1;
782 }
783 }
784 _ => {}
785 },
786 Step::ApiKey => match key.code {
787 KeyCode::Tab => self.step = Step::ApiHeader,
788 KeyCode::Enter => {
789 let _ = self.advance_step();
790 }
791 KeyCode::Char(c) => {
792 self.reuse_keyring_api_key = false;
793 let pos = self.api_key_cursor.min(self.api_key.len());
794 self.api_key.insert(pos, c);
795 self.api_key_cursor = pos + 1;
796 }
797 KeyCode::Backspace => {
798 if self.api_key_cursor > 0 && self.api_key_cursor <= self.api_key.len() {
799 self.api_key.remove(self.api_key_cursor - 1);
800 self.api_key_cursor -= 1;
801 }
802 }
803 KeyCode::Left => {
804 if self.api_key_cursor > 0 {
805 self.api_key_cursor -= 1;
806 }
807 }
808 KeyCode::Right => {
809 if self.api_key_cursor < self.api_key.len() {
810 self.api_key_cursor += 1;
811 }
812 }
813 _ => {}
814 },
815 Step::Summary => {
816 if key.code == KeyCode::Enter {
817 self.testing = true;
818 self.error = None;
819 }
822 }
823 }
824 Ok(false)
825 }
826
827 pub async fn run(mut self, verbose: bool) -> Result<Config> {
828 enable_raw_mode()?;
829 let mut stdout = stdout();
830 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
831 let backend = CrosstermBackend::new(stdout);
832 let mut terminal = Terminal::new(backend)?;
833
834 loop {
835 terminal.draw(|f| {
836 let area = f.size();
837 self.render(f, area);
838 if let Some((x, y)) = self.cursor_pos(area) {
839 f.set_cursor(x, y);
840 }
841 })?;
842
843 if event::poll(std::time::Duration::from_millis(100))? {
844 if let Event::Key(key) = event::read()? {
845 if self.handle_key(key)? {
846 disable_raw_mode()?;
847 execute!(
848 terminal.backend_mut(),
849 LeaveAlternateScreen,
850 DisableMouseCapture
851 )?;
852 terminal.show_cursor()?;
853 return Err(anyhow!("setup cancelled"));
854 }
855
856 if self.testing {
857 terminal.draw(|f| {
858 let area = f.size();
859 self.render(f, area);
860 })?;
861 let result = self.try_connect_and_persist(verbose).await;
862 self.testing = false;
863 match result {
864 Ok(cfg) => {
865 disable_raw_mode()?;
866 execute!(
867 terminal.backend_mut(),
868 LeaveAlternateScreen,
869 DisableMouseCapture
870 )?;
871 terminal.show_cursor()?;
872 return Ok(cfg);
873 }
874 Err(e) => {
875 self.error = Some(format!("{e:#}"));
876 }
877 }
878 }
879 }
880 }
881 }
882 }
883}
884
885impl Default for SetupWizard {
886 fn default() -> Self {
887 Self::new()
888 }
889}