| 1 | //! API key entry screen for onboarding. |
| 2 | |
| 3 | use ratatui::style::{Modifier, Style}; |
| 4 | use ratatui::text::{Line, Span}; |
| 5 | |
| 6 | use crate::palette; |
| 7 | use crate::tui::app::App; |
| 8 | |
| 9 | pub fn lines(app: &App) -> Vec<Line<'static>> { |
| 10 | let mut lines = vec![ |
| 11 | Line::from(Span::styled( |
| 12 | "Connect your DeepSeek API key", |
| 13 | Style::default() |
| 14 | .fg(palette::DEEPSEEK_SKY) |
| 15 | .add_modifier(Modifier::BOLD), |
| 16 | )), |
| 17 | Line::from(""), |
| 18 | Line::from(Span::styled( |
| 19 | "Step 1. Open https://platform.deepseek.com/api_keys and create a key.", |
| 20 | Style::default().fg(palette::TEXT_PRIMARY), |
| 21 | )), |
| 22 | Line::from(Span::styled( |
| 23 | "Step 2. Paste it below and press Enter.", |
| 24 | Style::default().fg(palette::TEXT_PRIMARY), |
| 25 | )), |
| 26 | Line::from(""), |
| 27 | Line::from(Span::styled( |
| 28 | "Saved to ~/.deepseek/config.toml so it works from any folder.", |
| 29 | Style::default().fg(palette::TEXT_MUTED), |
| 30 | )), |
| 31 | Line::from(Span::styled( |
| 32 | "Paste the full key exactly as issued (no spaces or newlines).", |
| 33 | Style::default().fg(palette::TEXT_MUTED), |
| 34 | )), |
| 35 | Line::from(""), |
| 36 | ]; |
| 37 | |
| 38 | let masked = mask_key(&app.api_key_input); |
| 39 | let display = if masked.is_empty() { |
| 40 | "(paste key here)" |
| 41 | } else { |
| 42 | masked.as_str() |
| 43 | }; |
| 44 | lines.push(Line::from(vec![ |
| 45 | Span::styled("Key: ", Style::default().fg(palette::TEXT_MUTED)), |
| 46 | Span::styled( |
| 47 | display.to_string(), |
| 48 | Style::default() |
| 49 | .fg(palette::TEXT_PRIMARY) |
| 50 | .add_modifier(Modifier::BOLD), |
| 51 | ), |
| 52 | ])); |
| 53 | lines.push(Line::from("")); |
| 54 | |
| 55 | if let Some(message) = app.status_message.as_deref() { |
| 56 | lines.push(Line::from(Span::styled( |
| 57 | message.to_string(), |
| 58 | Style::default().fg(palette::STATUS_WARNING), |
| 59 | ))); |
| 60 | lines.push(Line::from("")); |
| 61 | } |
| 62 | |
| 63 | lines.push(Line::from("")); |
| 64 | lines.push(Line::from(Span::styled( |
| 65 | "Press Enter to save, Esc to go back.", |
| 66 | Style::default().fg(palette::TEXT_MUTED), |
| 67 | ))); |
| 68 | |
| 69 | lines |
| 70 | } |
| 71 | |
| 72 | fn mask_key(input: &str) -> String { |
| 73 | let trimmed = input.trim(); |
| 74 | let len = trimmed.chars().count(); |
| 75 | if len == 0 { |
| 76 | return String::new(); |
| 77 | } |
| 78 | if len <= 4 { |
| 79 | return "*".repeat(len); |
| 80 | } |
| 81 | let visible: String = trimmed |
| 82 | .chars() |
| 83 | .rev() |
| 84 | .take(4) |
| 85 | .collect::<String>() |
| 86 | .chars() |
| 87 | .rev() |
| 88 | .collect(); |
| 89 | format!("{}{}", "*".repeat(len - 4), visible) |
| 90 | } |
| 91 |