zilliz 1.4.3

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
//! Persistent single-row status bar shared by every TUI screen.

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};

/// One left-zone hint: `<key> <label>`.
#[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,
                ..
            } => {
                // Rich identity format mirrors what the (removed) header
                // strip used to render. Auth0 → `<email> · <org> · <region>`;
                // API-key → `API key · <masked> · <region>`.
                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)),
        ]
    }
}

/// Per-screen hint mapping. The "lead" segment names the current context
/// (e.g. `sign in · global`), then the hints follow.
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)
}

/// Render the status bar into a single-row rect.
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
    let (lead, hints) = status_hints(app);
    let session = SessionIndicator::from_snapshot(&app.auth);

    // Build left spans: lead label + key hints, joined by " · ".
    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;
    // Reserve 2-col gutter between zones.
    let left_budget = total.saturating_sub(right_width + 2);

    // Truncate the left span list to fit `left_budget` columns.
    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 {
            // Append a single-char ellipsis if there's room.
            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);
}

/// Used by the help overlay to print one hint row per line.
#[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)),
    ])
}

// Silence unused-import warning if these types are later removed; keep
// imports available for future hint variants without churning.
#[allow(dead_code)]
fn _force_use(_: WizardAuthMethod, _: AppRegion) {}