use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use unicode_width::UnicodeWidthStr;
use crate::tui::app::{App, AuthMethod, AuthState, Region as AppRegion, Screen};
use crate::tui::wizard::{AuthMethod as WizardAuthMethod, Region as WizardRegion};
#[derive(Debug, Clone)]
pub struct KeyHint {
pub key: &'static str,
pub label: &'static str,
}
impl KeyHint {
pub const fn new(key: &'static str, label: &'static str) -> Self {
Self { key, label }
}
}
#[derive(Debug, Clone)]
pub struct SessionIndicator {
pub signed_in: bool,
pub label: String,
}
impl SessionIndicator {
pub fn from_snapshot(auth: &crate::tui::app::AuthSnapshot) -> Self {
match &auth.state {
AuthState::SignedOut => Self {
signed_in: false,
label: "no session".to_string(),
},
AuthState::SignedIn {
method,
user,
email,
org,
region,
masked_api_key,
..
} => {
let label = match method {
AuthMethod::Auth0 => {
let who = email
.clone()
.filter(|s| !s.is_empty())
.or_else(|| user.clone().filter(|s| !s.is_empty()))
.unwrap_or_else(|| "signed in".to_string());
match org.as_ref().filter(|s| !s.is_empty()) {
Some(o) => format!("{} · {} · {}", who, o, region.slug()),
None => format!("{} · {}", who, region.slug()),
}
}
AuthMethod::ApiKey => {
let masked = masked_api_key.clone().unwrap_or_else(|| "****".to_string());
format!("API key · {} · {}", masked, region.slug())
}
};
Self {
signed_in: true,
label,
}
}
}
}
fn render_spans(&self) -> Vec<Span<'static>> {
let dot_style = if self.signed_in {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
};
vec![
Span::styled("● ", dot_style),
Span::styled(self.label.clone(), Style::default().fg(Color::Gray)),
]
}
}
pub fn status_hints(app: &App) -> (String, Vec<KeyHint>) {
let lead = match app.current_screen() {
Screen::Home => {
if app.auth.is_signed_in() {
"signed in".to_string()
} else {
"offline".to_string()
}
}
Screen::SignInRegion => "sign in".to_string(),
Screen::SignInMethod => {
let region = app
.wizard
.as_ref()
.and_then(|w| w.region)
.map(WizardRegion::slug)
.unwrap_or("");
if region.is_empty() {
"sign in".to_string()
} else {
format!("sign in · {}", region)
}
}
Screen::SignInBrowser => {
let region = app
.wizard
.as_ref()
.and_then(|w| w.region)
.map(WizardRegion::slug)
.unwrap_or("global");
format!("sign in · {} · auth0", region)
}
Screen::SignInApiKey => {
let region = app
.wizard
.as_ref()
.and_then(|w| w.region)
.map(WizardRegion::slug)
.unwrap_or("global");
format!("sign in · {} · api key", region)
}
Screen::LogoutConfirm => "sign out".to_string(),
Screen::Help => "help".to_string(),
};
let hints: Vec<KeyHint> = match app.current_screen() {
Screen::Home => {
if app.auth.is_signed_in() {
vec![
KeyHint::new("l", "sign out"),
KeyHint::new("?", "help"),
KeyHint::new("q", "quit"),
]
} else {
vec![
KeyHint::new("l", "sign in"),
KeyHint::new("?", "help"),
KeyHint::new("q", "quit"),
]
}
}
Screen::SignInRegion => vec![
KeyHint::new("↑↓", "select"),
KeyHint::new("↵", "continue"),
KeyHint::new("esc", "cancel"),
],
Screen::SignInMethod => vec![
KeyHint::new("↑↓", "select"),
KeyHint::new("↵", "continue"),
KeyHint::new("b", "back"),
KeyHint::new("esc", "cancel"),
],
Screen::SignInBrowser => vec![
KeyHint::new("c", "copy code"),
KeyHint::new("o", "open browser"),
KeyHint::new("esc", "cancel"),
],
Screen::SignInApiKey => vec![
KeyHint::new("↵", "sign in"),
KeyHint::new("^V", "paste"),
KeyHint::new("^H", "show/hide"),
KeyHint::new("b", "back"),
KeyHint::new("esc", "cancel"),
],
Screen::LogoutConfirm => vec![
KeyHint::new("y", "confirm"),
KeyHint::new("n", "cancel"),
KeyHint::new("esc", "cancel"),
],
Screen::Help => vec![KeyHint::new("esc", "close")],
};
(lead, hints)
}
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
let (lead, hints) = status_hints(app);
let session = SessionIndicator::from_snapshot(&app.auth);
let mut left_spans: Vec<Span<'static>> = Vec::new();
let key_style = Style::default()
.fg(Color::White)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD);
let label_style = Style::default().fg(Color::Gray);
let sep_style = Style::default().fg(Color::DarkGray);
if !lead.is_empty() {
left_spans.push(Span::styled(lead.clone(), label_style));
}
for hint in &hints {
if !left_spans.is_empty() {
left_spans.push(Span::styled(" · ", sep_style));
}
left_spans.push(Span::styled(format!(" {} ", hint.key), key_style));
left_spans.push(Span::raw(" "));
left_spans.push(Span::styled(hint.label, label_style));
}
let right_spans = session.render_spans();
let right_width: usize = right_spans
.iter()
.map(|s| UnicodeWidthStr::width(s.content.as_ref()))
.sum();
let total = area.width as usize;
let left_budget = total.saturating_sub(right_width + 2);
let mut truncated_left: Vec<Span<'static>> = Vec::new();
let mut used: usize = 0;
for span in left_spans.into_iter() {
let w = UnicodeWidthStr::width(span.content.as_ref());
if used + w > left_budget {
if left_budget > used + 1 {
truncated_left.push(Span::styled("…", label_style));
}
break;
}
used += w;
truncated_left.push(span);
}
let pad_width = total.saturating_sub(used + right_width).max(1);
let mut all_spans = truncated_left;
all_spans.push(Span::raw(" ".repeat(pad_width)));
all_spans.extend(right_spans);
let line = Line::from(all_spans);
let para = Paragraph::new(line).style(Style::default().bg(Color::Reset));
frame.render_widget(para, area);
}
#[allow(dead_code)]
pub fn hint_row(hint: &KeyHint) -> Line<'static> {
Line::from(vec![
Span::styled(
format!(" {} ", hint.key),
Style::default()
.fg(Color::White)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(hint.label, Style::default().fg(Color::Gray)),
])
}
#[allow(dead_code)]
fn _force_use(_: WizardAuthMethod, _: AppRegion) {}