use bcode_provider_auth_models::AuthPoolSummary;
use bmux_tui::prelude::Line as ListItem;
use bmux_tui::prelude::{Line, Span, Style};
use bmux_tui::style::Modifier;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthPoolPickerApp {
pools: Vec<AuthPoolSummary>,
rows: Vec<(usize, usize)>,
list: super::filtered_list::FilteredListState,
}
impl AuthPoolPickerApp {
#[must_use]
pub fn new(pools: Vec<AuthPoolSummary>) -> Self {
let rows = pools
.iter()
.enumerate()
.flat_map(|(pool_index, pool)| {
(0..pool.profiles.len()).map(move |profile_index| (pool_index, profile_index))
})
.collect::<Vec<_>>();
let list = super::filtered_list::FilteredListState::new(rows.len());
Self { pools, rows, list }
}
pub fn render_list(
&mut self,
items: &[ListItem],
area: bmux_tui::geometry::Rect,
frame: &mut bmux_tui::paint::PaintCx<'_, '_>,
theme: super::render::TuiTheme,
) {
super::picker_render::render_picker_list(
items,
self.list.render_state(area.height),
area,
frame,
theme,
);
}
#[must_use]
pub fn list_items(&self, muted: Style, accent: Style) -> Vec<ListItem> {
if self.rows.is_empty() {
return vec![Line::from("No auth pools configured.")];
}
self.rows
.iter()
.map(|(pool_index, profile_index)| {
let pool = &self.pools[*pool_index];
let profile = &pool.profiles[*profile_index];
let marker = if profile.preferred { "preferred" } else { "" };
let availability = if profile.cooldown {
"cooldown"
} else {
"available"
};
Line::from_spans(vec![
Span::styled(pool.pool.clone(), Style::new().add_modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled(profile.profile.clone(), accent),
Span::raw(" "),
Span::styled(marker.to_owned(), muted),
Span::raw(" "),
Span::styled(availability.to_owned(), muted),
Span::raw(" "),
Span::styled(
pool.automatic_group.as_ref().map_or_else(
|| "manual".to_owned(),
|group| format!("automatic ({group})"),
),
muted,
),
])
})
.collect()
}
pub fn select_next(&mut self) {
if self.rows.is_empty() {
return;
}
self.list.select_next();
}
pub fn select_previous(&mut self) {
if self.rows.is_empty() {
return;
}
self.list.select_previous();
}
pub const fn select_visible(&mut self, row: usize) -> bool {
if row >= self.rows.len() {
return false;
}
self.list.select_visible(row)
}
#[must_use]
pub fn selected(&self) -> Option<(String, String)> {
let (pool_index, profile_index) = *self.rows.get(self.list.selected_source_index()?)?;
let pool = &self.pools[pool_index];
Some((
pool.pool.clone(),
pool.profiles[profile_index].profile.clone(),
))
}
#[must_use]
pub fn selected_pool(&self) -> Option<String> {
self.selected().map(|(pool, _)| pool)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bcode_provider_auth_models::{
AUTH_POOL_SCHEMA_VERSION, AuthPoolPreferenceSource, AuthPoolProfileSummary,
};
fn pool() -> AuthPoolSummary {
AuthPoolSummary {
schema_version: AUTH_POOL_SCHEMA_VERSION,
pool: "provider".to_owned(),
provider_plugin_id: Some("plugin".to_owned()),
automatic_group: None,
strategy: "failover".to_owned(),
preferred_profile: Some("one".to_owned()),
preference_source: Some(AuthPoolPreferenceSource::PoolOrder),
profiles: vec![
AuthPoolProfileSummary {
profile: "one".to_owned(),
preferred: true,
cooldown: false,
cooldown_until_unix: None,
},
AuthPoolProfileSummary {
profile: "two".to_owned(),
preferred: false,
cooldown: false,
cooldown_until_unix: None,
},
],
degraded_reason: None,
}
}
#[test]
fn picker_rows_report_manual_and_automatic_management_without_changing_selection() {
let manual = AuthPoolPickerApp::new(vec![pool()]);
let text = |picker: &AuthPoolPickerApp| {
picker.list_items(Style::new(), Style::new())[0]
.spans
.iter()
.map(|span| span.content.as_str())
.collect::<String>()
};
assert!(text(&manual).contains("manual"));
let mut automatic_pool = pool();
automatic_pool.automatic_group = Some("subscription".to_owned());
let automatic = AuthPoolPickerApp::new(vec![automatic_pool]);
assert!(text(&automatic).contains("automatic (subscription)"));
assert_eq!(manual.selected(), automatic.selected());
}
#[test]
fn picker_selects_any_profile_in_an_n_profile_pool() {
let mut picker = AuthPoolPickerApp::new(vec![pool()]);
assert_eq!(
picker.selected(),
Some(("provider".to_owned(), "one".to_owned()))
);
picker.select_next();
assert_eq!(
picker.selected(),
Some(("provider".to_owned(), "two".to_owned()))
);
assert_eq!(picker.selected_pool().as_deref(), Some("provider"));
}
}