use super::theme::PresentedTheme;
use bcode_plugin_sdk::tui::keymap::controls::{ControlAction, ControlInput};
use bmux_keyboard::KeyCode;
use bmux_text_edit::TextEditBuffer;
use bmux_tui::component::{Component, Constraints, LayoutCx};
use bmux_tui::event::Event;
#[cfg(test)]
use bmux_tui::frame::Frame;
use bmux_tui::geometry::Rect;
use bmux_tui::paint::{LocalRect, PaintCx};
use bmux_tui::prelude::{Line, Span, Style, TextBlock, TextWrap};
use bmux_tui_components::text_input::TextInputComponent;
use bmux_tui_components::text_input::{TextInputControl, TextInputPolicy, TextInputState};
use std::collections::BTreeMap;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Presentation {
Guided,
Advanced,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EnrollmentMode {
Quick,
Configurable,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EnrollmentStatus {
Pending,
Completed,
}
pub struct ConnectionForm {
keymap: super::keymap::BmuxKeyMap,
presentation: Presentation,
providers: Vec<bcode_provider_auth_models::AuthProviderContribution>,
picker: Option<bool>,
selected: usize,
query: String,
interactive: bool,
device: Option<super::setup_device_login::DeviceLogin>,
importing: bool,
fields: [TextInputState; 4],
focus: usize,
secret: zeroize::Zeroizing<String>,
review: bool,
pending_declaration: Option<bcode_config::edit::ConfigEdit>,
status: String,
completed: EnrollmentStatus,
mode: EnrollmentMode,
}
impl ConnectionForm {
pub fn new(importing: bool) -> Self {
let (providers, status) = match load_connection_choices() {
Ok(providers) if providers.is_empty() => (
providers,
"No enabled authentication provider is available. Use Advanced to review plugins."
.to_owned(),
),
Ok(providers) => (
providers,
"Choose a provider to connect. Cancel returns.".to_owned(),
),
Err(message) => (Vec::new(), message),
};
Self {
keymap: super::keymap::BmuxKeyMap::from_registry(
bcode_plugin_sdk::tui::keymap::KeymapRegistry::default(),
),
presentation: Presentation::Guided,
providers,
picker: Some(false),
selected: 0,
query: String::new(),
interactive: false,
device: None,
importing,
fields: [
String::new(),
"api_key".to_owned(),
String::new(),
bcode_config::default_auth_vault_path()
.display()
.to_string(),
]
.map(|text| TextInputState::new(TextEditBuffer::from_text(text))),
focus: 0,
secret: zeroize::Zeroizing::new(String::new()),
review: false,
pending_declaration: None,
status,
completed: EnrollmentStatus::Pending,
mode: EnrollmentMode::Configurable,
}
}
#[must_use]
pub fn quick() -> Self {
let mut form = Self::new(false);
form.mode = EnrollmentMode::Quick;
form
}
pub fn completion(&mut self) -> Option<(String, String)> {
if let Some(device) = &mut self.device {
device.refresh();
if device.succeeded {
self.completed = EnrollmentStatus::Completed;
}
}
(self.completed == EnrollmentStatus::Completed).then(|| {
(
self.fields[0].buffer().text().to_owned(),
self.fields[2].buffer().text().to_owned(),
)
})
}
#[cfg(test)]
pub fn handle_event(&mut self, event: &Event) -> bool {
self.handle_event_with_keymap(
event,
&super::keymap::BmuxKeyMap::from_config(&bcode_config::TuiConfig::default()),
)
}
pub fn handle_event_with_keymap(
&mut self,
event: &Event,
keymap: &super::keymap::BmuxKeyMap,
) -> bool {
self.keymap = keymap.clone();
let mapped;
let event = if let Event::Key(stroke) = event {
let supported = ControlAction::TEXT
.iter()
.copied()
.chain([
ControlAction::Submit,
ControlAction::Cancel,
ControlAction::FocusNext,
ControlAction::FocusPrevious,
ControlAction::PageUp,
ControlAction::PageDown,
])
.collect::<Vec<_>>();
let scope = if self.device.as_ref().is_some_and(|device| device.terminal) {
"setup_auth_terminal"
} else if self.device.is_some() {
"setup_auth"
} else {
"setup_connection"
};
let native = if keymap.action(scope, *stroke) == Some("tui.setup.advanced") {
Some(bmux_keyboard::KeyStroke::simple(KeyCode::F(2)))
} else {
ControlInput::from_registry(keymap.registry(), scope, *stroke, &supported, true)
.native_stroke()
};
let native = if self.device.as_ref().is_some_and(|device| device.terminal) {
match keymap.action(scope, *stroke) {
Some("tui.setup.retry") => {
Some(bmux_keyboard::KeyStroke::simple(KeyCode::Char('r')))
}
Some("tui.setup.back") => {
Some(bmux_keyboard::KeyStroke::simple(KeyCode::Escape))
}
Some("tui.control.submit" | "tui.control.cancel") => native,
_ => None,
}
} else {
native
};
let Some(native) = native else {
return false;
};
mapped = Event::Key(native);
&mapped
} else {
event
};
self.handle_admitted_event(event)
}
fn handle_admitted_event(&mut self, event: &Event) -> bool {
if self.pending_declaration.is_some() {
self.handle_declaration_review(event);
return false;
}
if self.picker.is_some() {
return self.handle_picker(event);
}
if self.device.is_some() {
return self.handle_device_event(event);
}
match event {
Event::Key(key) if key.key == KeyCode::Escape => {
if self.review {
self.review = false;
} else {
return true;
}
}
Event::Key(key)
if key.key == KeyCode::F(2) && self.mode == EnrollmentMode::Configurable =>
{
self.presentation = if self.presentation == Presentation::Guided {
Presentation::Advanced
} else {
Presentation::Guided
};
self.focus = if self.presentation == Presentation::Advanced {
2
} else {
4
};
}
Event::Key(key) if key.key == KeyCode::Tab && !self.review => {
self.focus = if self.presentation == Presentation::Advanced {
if key.modifiers.shift {
(self.focus + 4) % 5
} else {
(self.focus + 1) % 5
}
} else {
4
};
}
Event::Key(key) if key.key == KeyCode::Enter => {
let confirm =
self.review || (self.mode == EnrollmentMode::Quick && !self.interactive);
if confirm && self.prepare_declaration_review() {
return false;
}
if confirm && !self.importing && self.interactive {
self.start_device_login();
} else if confirm {
match self.save() {
Ok(message) => {
self.completed = EnrollmentStatus::Completed;
self.status = message;
}
Err(message) => self.status = message,
}
self.secret.clear();
self.review = false;
} else {
self.review = true;
"Review destination and provider. Confirm saves; Cancel cancels. No remote verification is performed.".clone_into(&mut self.status);
}
}
Event::Key(key) if !self.review && self.focus == 4 => match key.key {
KeyCode::Char(character) if self.secret.len() < 4096 => self.secret.push(character),
KeyCode::Backspace => {
self.secret.pop();
}
_ => {}
},
Event::Paste(text) if !self.review && self.focus == 4 => {
let remaining = 4096_usize.saturating_sub(self.secret.len());
self.secret
.extend(text.chars().filter(|c| !c.is_control()).take(remaining));
}
Event::Key(key) if !self.review => {
let _ = TextInputControl::new(&TextInputPolicy::default())
.handle_key(&mut self.fields[self.focus], *key);
}
Event::Paste(text) if !self.review => {
let _ = TextInputControl::new(&TextInputPolicy::default())
.handle_paste(&mut self.fields[self.focus], text);
}
_ => {}
}
false
}
fn handle_device_event(&mut self, event: &Event) -> bool {
if let Some(device) = &mut self.device {
device.refresh();
handle_auth_prompt(device, event);
if device.terminal {
if matches!(event, Event::Key(key) if matches!(key.key, KeyCode::Enter | KeyCode::Escape | KeyCode::Char('r' | 'b' | 'q')))
{
if device.succeeded {
return true;
}
self.device = None;
self.review = false;
self.picker = Some(true);
self.query.clear();
self.selected = 0;
"Choose a method to retry or Cancel to choose another provider."
.clone_into(&mut self.status);
}
} else if matches!(event, Event::Key(key) if matches!(key.key, KeyCode::Escape)) {
device.cancel();
}
}
false
}
fn start_device_login(&mut self) {
self.device = Some(super::setup_device_login::DeviceLogin::start(
self.fields[0].buffer().text().to_owned(),
self.fields[1].buffer().text().to_owned(),
self.fields[2].buffer().text().to_owned(),
self.fields[3].buffer().text().to_owned(),
));
self.review = false;
}
fn prepare_declaration_review(&mut self) -> bool {
if self.mode == EnrollmentMode::Quick {
return match bcode_config::load_config() {
Ok(config) if config.active_context.is_some() => {
"This explicit context requires a declared account. Use Advanced to review configuration; no credentials changed.".clone_into(&mut self.status);
true
}
Ok(_) if self.fields[2].buffer().text().is_empty() => {
"Could not allocate an account profile. Return to provider selection or use Advanced; no credentials changed.".clone_into(&mut self.status);
true
}
Ok(_) => false,
Err(_) => {
"Configuration unavailable; no credentials changed."
.clone_into(&mut self.status);
true
}
};
}
match self.context_declaration() {
Ok(Some(edit)) => {
self.status = format!(
"Create context-local account declaration in {}? Confirm configuration only; Cancel cancels. Sign-in starts separately.",
edit.path().display()
);
self.pending_declaration = Some(edit);
true
}
Err(message) => {
self.status = message;
true
}
Ok(None) => false,
}
}
fn handle_declaration_review(&mut self, event: &Event) {
match event {
Event::Key(key) if key.key == KeyCode::Escape => {
self.pending_declaration = None;
"Declaration cancelled; no credentials changed.".clone_into(&mut self.status);
}
Event::Key(key) if key.key == KeyCode::Enter => {
if let Some(edit) = self.pending_declaration.take() {
self.status = match edit.apply() {
Ok(_) => {
"Account declaration saved. Confirm again to start sign-in.".to_owned()
}
Err(_) => {
"Could not save declaration safely. Review configuration and retry."
.to_owned()
}
};
}
}
_ => {}
}
}
fn context_declaration(&self) -> Result<Option<bcode_config::edit::ConfigEdit>, String> {
let config =
bcode_config::load_config().map_err(|_| "Cannot load configuration".to_owned())?;
let Some(context) = &config.active_context else {
return Ok(None);
};
let local = self.fields[2].buffer().text();
let qualified = bcode_config::contexts::qualify(context, local)
.map_err(|_| "Invalid context-local account name".to_owned())?;
if config.auth.profiles.contains_key(&qualified) {
return Ok(None);
}
let selection = bcode_config::plugin_selection_with_default_plugin_ids(
&config,
std::iter::empty::<&str>(),
);
let mut host = bcode_plugin::PluginHost::load_defaults_with_static_bundled(
&selection,
&super::static_bundled_plugins(),
)
.map_err(|_| "Cannot load authentication providers".to_owned())?;
let result = (|| {
let provider = host
.auth_provider_registry()
.get(self.fields[0].buffer().text())
.ok_or_else(|| "Unknown provider".to_owned())?;
bcode_provider_auth::enrollment::plan_context_account(
bcode_config::default_config_dir().join("bcode.toml"),
context,
local,
&provider.contribution,
&provider.plugin_id,
self.fields[1].buffer().text(),
self.fields[3].buffer().text().into(),
)
.map(Some)
})();
host.deactivate_all()
.map_err(|_| "Provider cleanup failed".to_owned())?;
result
}
fn choices(&self) -> Vec<(usize, String)> {
let labels = if self.picker == Some(true) {
self.providers
.iter()
.find(|provider| provider.provider_id == self.fields[0].buffer().text())
.map(|provider| {
provider
.methods
.iter()
.map(|method| match method {
bcode_provider_auth_models::AuthMethodContribution::SecretFields {
display_name,
..
}
| bcode_provider_auth_models::AuthMethodContribution::Interactive {
display_name,
..
} => display_name.clone(),
})
.collect()
})
.unwrap_or_default()
} else {
self.providers
.iter()
.map(|provider| provider.display_name.clone())
.collect::<Vec<_>>()
};
labels
.into_iter()
.enumerate()
.filter(|(_, label)| label.to_lowercase().contains(&self.query.to_lowercase()))
.collect()
}
fn handle_picker(&mut self, event: &Event) -> bool {
match event {
Event::Key(key) => match key.key {
KeyCode::Escape => {
if self.picker == Some(true) {
self.picker = Some(false);
self.query.clear();
self.selected = 0;
} else {
return true;
}
}
KeyCode::Char(character) => {
self.query.push(character);
self.selected = 0;
}
KeyCode::Backspace => {
self.query.pop();
self.selected = 0;
}
KeyCode::Down => self.selected = self.selected.saturating_add(1),
KeyCode::Up => self.selected = self.selected.saturating_sub(1),
KeyCode::Enter => self.select_choice(),
_ => {}
},
Event::Paste(text) => {
self.query.extend(
text.chars()
.filter(|character| !character.is_control())
.take(256),
);
self.selected = 0;
}
_ => {}
}
false
}
fn select_choice(&mut self) {
let choices = self.choices();
let Some((index, _)) = choices.get(self.selected.min(choices.len().saturating_sub(1)))
else {
return;
};
if self.picker == Some(false) {
let provider = &self.providers[*index];
self.fields[0] =
TextInputState::new(TextEditBuffer::from_text(provider.provider_id.clone()));
if let Ok(config) = bcode_config::load_config() {
let name = bcode_provider_auth::enrollment::new_profile_name(
&config,
&bcode_config::load_runtime_auth_subscriptions(),
&provider.provider_id,
);
self.fields[2] = TextInputState::new(TextEditBuffer::from_text(name));
}
self.picker = Some(true);
} else {
let Some(provider) = self
.providers
.iter()
.find(|provider| provider.provider_id == self.fields[0].buffer().text())
else {
return;
};
let method = &provider.methods[*index];
self.interactive = matches!(
method,
bcode_provider_auth_models::AuthMethodContribution::Interactive { .. }
);
self.fields[1] =
TextInputState::new(TextEditBuffer::from_text(method.method_id().to_owned()));
self.focus = 4;
self.review = self.interactive;
"Confirm connection using the selected method and default secure destination. Next field reviews advanced fields.".clone_into(&mut self.status);
self.picker = None;
}
self.query.clear();
self.selected = 0;
}
fn save(&self) -> Result<String, String> {
let config =
bcode_config::load_config().map_err(|_| "Cannot load configuration".to_owned())?;
let selection = bcode_config::plugin_selection_with_default_plugin_ids(
&config,
std::iter::empty::<&str>(),
);
let mut host = bcode_plugin::PluginHost::load_defaults_with_static_bundled(
&selection,
&super::static_bundled_plugins(),
)
.map_err(|_| "Cannot load enabled authentication providers".to_owned())?;
let result = self.save_with_host(&config, &host);
let cleanup = host
.deactivate_all()
.map_err(|_| "Authentication provider cleanup failed".to_owned());
result.and_then(|message| cleanup.map(|()| message))
}
fn save_with_host(
&self,
config: &bcode_config::BcodeConfig,
host: &bcode_plugin::PluginHost,
) -> Result<String, String> {
let provider_id = self.fields[0].buffer().text();
let method_id = self.fields[1].buffer().text();
let provider = host
.auth_provider_registry()
.get(provider_id)
.ok_or_else(|| "Unknown or disabled authentication provider".to_owned())?;
let method = provider
.contribution
.methods
.iter()
.find(|method| method.method_id() == method_id)
.ok_or_else(|| "Unknown authentication method".to_owned())?;
let bcode_provider_auth_models::AuthMethodContribution::SecretFields { fields, .. } =
method
else {
return Err(
"This method requires an interactive authentication flow; no credentials saved."
.to_owned(),
);
};
if fields.len() != 1 {
return Err(
"This method requires multiple credential fields; no credentials saved.".to_owned(),
);
}
let imported;
let secret = if self.importing {
let index = self.secret.parse::<usize>().map_err(|_| {
"Enter a provider-declared source index in the source field".to_owned()
})?;
let home =
std::env::home_dir().ok_or_else(|| "Home directory unavailable".to_owned())?;
imported = bcode_provider_auth::discovery::read_selected(&home, &fields[0], index)
.map_err(|_| "Source is unavailable or incompatible".to_owned())?
.ok_or_else(|| "Credential source is empty".to_owned())?;
imported.as_str()
} else {
self.secret.as_str()
};
fields[0]
.validation
.validate_secret(secret)
.map_err(|_| "Credential does not meet provider requirements".to_owned())?;
let profile = self.fields[2].buffer().text();
let vault = self.fields[3].buffer().text();
let runtime = bcode_config::try_load_runtime_auth_subscriptions().map_err(|_| {
"Authentication metadata is unreadable or unsupported. Inspect authentication state before saving credentials.".to_owned()
})?;
let compatibility =
super::setup_device_login::pool_compatibility(host, provider).map_err(str::to_owned)?;
let plan = bcode_provider_auth::enrollment::plan(
config,
&runtime,
&provider.contribution,
&provider.plugin_id,
method_id,
bcode_provider_auth::enrollment::EnrollmentDestination {
profile: (!profile.is_empty()).then(|| profile.to_owned()),
vault: (!vault.is_empty()).then(|| vault.into()),
recipient_key: None,
},
bcode_provider_auth::enrollment::PoolEnrollment::Automatic,
compatibility.as_ref(),
)
.map_err(|_| "Profile ownership or destination is inconsistent".to_owned())?;
let prepared = &plan.prepared;
let lifecycle = bcode_provider_auth::lifecycle::AuthVaultLifecycle::new(
&prepared.resolved,
provider_id,
&provider.plugin_id,
method,
)
.map_err(|_| "Profile method or ownership is inconsistent".to_owned())?;
lifecycle.import_new(BTreeMap::from([(fields[0].credential_id.clone(), secret.to_owned())]))
.map_err(|_| "Could not save without overwriting existing credentials. Choose a new profile or inspect the vault.".to_owned())?;
let completion = bcode_provider_auth::enrollment::complete(&plan).map_err(|_| "Credential saved, but authentication metadata publication failed. Inspect authentication state before retrying; do not repeat sign-in merely to repair metadata.".to_owned())?;
Ok(format!(
"Account connected. {}",
super::setup_device_login::completion_message(&completion)
))
}
fn render_picker(&self, frame: &mut PaintCx<'_, '_>, theme: &PresentedTheme) {
let area = Rect::new(0, 0, frame.area().width, frame.area().height);
write(
frame,
area,
1,
if self.picker == Some(true) {
"How would you like to connect?"
} else {
"Choose a provider"
},
theme.focused,
);
write(
frame,
area,
3,
&format!("Search: {}", self.query),
theme.text,
);
let choices = self.choices();
let selected = self.selected.min(choices.len().saturating_sub(1));
let height = usize::from(area.height.saturating_sub(8));
let start = selected.saturating_sub(height.saturating_sub(1));
for (row, (_, label)) in choices.iter().enumerate().skip(start).take(height) {
write(
frame,
area,
5 + u16::try_from(row - start).unwrap_or(0),
&format!("{} {label}", if row == selected { ">" } else { " " }),
if row == selected {
theme.focused
} else {
theme.text
},
);
}
if choices.is_empty() {
write(
frame,
area,
5,
"No matching enabled providers or methods.",
theme.muted,
);
}
write(
frame,
area,
area.height.saturating_sub(3),
&self.status,
theme.muted,
);
write(
frame,
area,
area.height.saturating_sub(2),
&format!(
"Type to search • {} select • {} continue • {} back",
self.keymap.hint("setup_connection", "tui.control.down"),
self.keymap.hint("setup_connection", "tui.control.submit"),
self.keymap.hint("setup_connection", "tui.control.cancel")
),
theme.muted,
);
}
fn render_guided(&self, frame: &mut PaintCx<'_, '_>, theme: &PresentedTheme) {
let area = Rect::new(0, 0, frame.area().width, frame.area().height);
let provider = self
.providers
.iter()
.find(|provider| provider.provider_id == self.fields[0].buffer().text());
let name = provider.map_or("Connection", |provider| provider.display_name.as_str());
write(frame, area, 1, name, theme.focused);
write(
frame,
area,
3,
if self.interactive {
"Ready to start browser authorization"
} else {
"Enter your credential (paste supported)"
},
theme.text,
);
if !self.interactive {
write(
frame,
area,
5,
if self.secret.is_empty() {
"Credential: (empty)"
} else {
"Credential: ********"
},
theme.text,
);
}
write(
frame,
area,
7,
&format!("Secure profile: {}", self.fields[2].buffer().text()),
theme.muted,
);
write(
frame,
area,
8,
&format!("Vault: {}", self.fields[3].buffer().text()),
theme.muted,
);
write(
frame,
area,
area.height.saturating_sub(3),
&format!(
"{} confirm • {} Advanced • {} back",
self.keymap.hint("setup_connection", "tui.control.submit"),
self.keymap.hint("setup_connection", "tui.setup.advanced"),
self.keymap.hint("setup_connection", "tui.control.cancel")
),
theme.focused,
);
write(
frame,
area,
area.height.saturating_sub(2),
&self.status,
theme.text,
);
}
fn render_device(&mut self, frame: &mut PaintCx<'_, '_>, theme: &PresentedTheme) {
if let Some(device) = &mut self.device {
device.refresh();
let area = Rect::new(0, 0, frame.area().width, frame.area().height);
if render_auth_prompt(device, frame, area, theme, &self.keymap) {
return;
}
render_login_text(frame, area, &device.lines, theme.text);
write(
frame,
area,
area.height.saturating_sub(2),
&if device.terminal {
format!(
"{} retry · {} back",
self.keymap.hint("setup_auth_terminal", "tui.setup.retry"),
self.keymap
.hint("setup_auth_terminal", "tui.control.cancel")
)
} else {
format!(
"{} cancel sign-in",
self.keymap.hint("setup_auth", "tui.control.cancel")
)
},
theme.focused,
);
}
}
pub fn render_with_keymap(
&mut self,
frame: &mut PaintCx<'_, '_>,
theme: &PresentedTheme,
keymap: &super::keymap::BmuxKeyMap,
) {
self.keymap = keymap.clone();
if self.picker.is_some() {
self.render_picker(frame, theme);
return;
}
if self.device.is_some() {
self.render_device(frame, theme);
return;
}
if self.presentation == Presentation::Guided {
self.render_guided(frame, theme);
return;
}
let area = Rect::new(0, 0, frame.area().width, frame.area().height);
let labels = [
"Provider",
"Method",
"Profile (empty: provider default)",
"Vault",
];
write(
frame,
area,
0,
if self.importing {
"Import credential — enter the provider-declared source index in the secret field"
} else {
"Connect provider — credentials remain masked"
},
theme.focused,
);
for (index, input) in self.fields.iter_mut().enumerate() {
let row = 2 + u16::try_from(index).unwrap_or(0) * 2;
write(frame, area, row, labels[index], theme.muted);
let rect = Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(row + 1),
area.width.saturating_sub(2),
1,
);
input.set_content_area(rect, &TextInputPolicy::default());
let retained = std::cell::RefCell::new(input.clone());
let policy = TextInputPolicy::default();
let editor = TextInputComponent::new(
format!("setup_connection_form.{index}"),
&retained,
&policy,
)
.style(theme.text)
.selection_style(theme.focused)
.focused(self.focus == index && !self.review);
let layout = editor.layout(Constraints::tight(rect.size()), &mut LayoutCx::new());
frame.with_child(
i32::from(rect.x),
i64::from(rect.y),
LocalRect::new(0, 0, rect.width, rect.height),
|cx| editor.paint(&layout, cx),
);
*input = retained.into_inner();
input.set_content_area(rect, &policy);
}
write(
frame,
area,
10,
if self.secret.is_empty() {
"Secret: (empty)"
} else {
"Secret: ********"
},
theme.text,
);
write(
frame,
area,
area.height.saturating_sub(2),
&self.status,
theme.muted,
);
}
}
fn render_auth_prompt(
device: &super::setup_device_login::DeviceLogin,
frame: &mut PaintCx<'_, '_>,
area: Rect,
theme: &PresentedTheme,
keymap: &super::keymap::BmuxKeyMap,
) -> bool {
if let Some(bcode_provider_auth_models::AuthFlowEffect::Prompt {
message, choices, ..
}) = &device.prompt
{
write(frame, area, 1, message, theme.focused);
for (index, choice) in choices
.iter()
.enumerate()
.take(usize::from(area.height.saturating_sub(7)))
{
write(
frame,
area,
3 + u16::try_from(index).unwrap_or(0),
choice,
theme.text,
);
}
write(
frame,
area,
area.height.saturating_sub(3),
&format!("Answer: {}", device.answer),
theme.text,
);
write(
frame,
area,
area.height.saturating_sub(2),
&format!(
"{} submits • {} cancels sign-in",
keymap.hint("setup_auth", "tui.control.submit"),
keymap.hint("setup_auth", "tui.control.cancel")
),
theme.muted,
);
return true;
}
false
}
fn handle_auth_prompt(device: &mut super::setup_device_login::DeviceLogin, event: &Event) {
if device.prompt.is_some() {
match event {
Event::Key(key) => match key.key {
KeyCode::Enter => device.submit_answer(),
KeyCode::Char(character) if device.answer.len() < 4096 => {
device.answer.push(character);
}
KeyCode::Backspace => {
device.answer.pop();
}
_ => {}
},
Event::Paste(text) => {
let remaining = 4096_usize.saturating_sub(device.answer.len());
device
.answer
.extend(text.chars().filter(|c| !c.is_control()).take(remaining));
}
_ => {}
}
}
}
fn load_connection_choices()
-> Result<Vec<bcode_provider_auth_models::AuthProviderContribution>, String> {
let config = bcode_config::load_config().map_err(|_| "Configuration unavailable".to_owned())?;
let selection =
bcode_config::plugin_selection_with_default_plugin_ids(&config, std::iter::empty::<&str>());
let mut host = bcode_plugin::PluginHost::load_defaults_with_static_bundled(
&selection,
&super::static_bundled_plugins(),
)
.map_err(|_| "Provider registration unavailable".to_owned())?;
let mut providers = host
.auth_provider_registry()
.providers()
.into_iter()
.map(|provider| provider.contribution.clone())
.collect::<Vec<_>>();
providers.sort_by(|left, right| left.display_name.cmp(&right.display_name));
host.deactivate_all()
.map_err(|_| "Provider cleanup failed".to_owned())?;
Ok(providers)
}
fn render_login_text(frame: &mut PaintCx<'_, '_>, area: Rect, lines: &[String], style: Style) {
let content = Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(2),
area.width.saturating_sub(2),
area.height.saturating_sub(5),
);
let text = TextBlock::new(lines.join("\n"))
.style(style)
.wrap(TextWrap::Word);
let layout = text.layout(Constraints::tight(content.size()), &mut LayoutCx::new());
frame.with_child(
i32::from(content.x),
i64::from(content.y),
LocalRect::new(0, 0, content.width, content.height),
|cx| text.paint(&layout, cx),
);
}
fn write(frame: &mut PaintCx<'_, '_>, area: Rect, row: u16, text: &str, style: Style) {
if row < area.height {
frame.write_line_with_fallback_style(
LocalRect::terminal(Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(row),
area.width.saturating_sub(2),
1,
)),
&Line::from_spans(vec![Span::styled(text, style)]),
Style::new(),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn authentication_error_wraps_inside_pane_without_covering_controls() {
let area = Rect::new(4, 3, 34, 14);
let mut buffer = bmux_tui::buffer::Buffer::empty(Rect::new(0, 0, 50, 20));
let text = "The authentication provider rejected or could not complete the request. Return to method selection to retry.";
render_login_text(
&mut PaintCx::new(&mut Frame::new(&mut buffer)),
area,
&[text.to_owned()],
Style::new(),
);
let rows = (area.y + 2..area.bottom() - 3)
.map(|y| buffer.row_symbols(y).expect("row"))
.collect::<Vec<_>>();
assert!(rows.iter().filter(|row| !row.trim().is_empty()).count() > 1);
assert!(rows.join(" ").contains("retry."));
assert!(
buffer
.row_symbols(area.bottom() - 2)
.expect("footer")
.trim()
.is_empty()
);
}
#[test]
fn connected_login_returns_to_setup_instead_of_retrying() {
let mut device = super::super::setup_device_login::DeviceLogin::failed_for_test();
device.refresh();
device.succeeded = true;
let mut form = ConnectionForm {
keymap: super::super::keymap::BmuxKeyMap::from_config(
&bcode_config::TuiConfig::default(),
),
presentation: Presentation::Guided,
providers: Vec::new(),
picker: None,
selected: 0,
query: String::new(),
interactive: true,
device: Some(device),
importing: false,
fields: std::array::from_fn(|_| {
TextInputState::new(TextEditBuffer::from_text(String::new()))
}),
focus: 4,
secret: zeroize::Zeroizing::new(String::new()),
review: false,
pending_declaration: None,
completed: EnrollmentStatus::Pending,
mode: EnrollmentMode::Configurable,
status: String::new(),
};
assert!(
form.handle_event(&Event::Key(bmux_keyboard::KeyStroke::simple(
KeyCode::Enter,
)))
);
assert_eq!(form.picker, None);
assert!(form.completion().is_some());
}
#[test]
fn cancelled_form_has_no_success_reference() {
let mut form = ConnectionForm {
keymap: super::super::keymap::BmuxKeyMap::from_config(
&bcode_config::TuiConfig::default(),
),
presentation: Presentation::Guided,
providers: Vec::new(),
picker: None,
selected: 0,
query: String::new(),
interactive: false,
device: None,
importing: false,
fields: std::array::from_fn(|_| {
TextInputState::new(TextEditBuffer::from_text(String::new()))
}),
focus: 4,
secret: zeroize::Zeroizing::new(String::new()),
review: false,
pending_declaration: None,
completed: EnrollmentStatus::Pending,
mode: EnrollmentMode::Quick,
status: String::new(),
};
assert!(
form.handle_event(&Event::Key(bmux_keyboard::KeyStroke::simple(
KeyCode::Escape
)))
);
assert!(form.completion().is_none());
}
#[test]
fn failed_login_can_return_to_method_selection_without_closing_tui() {
for key in [
KeyCode::Enter,
KeyCode::Escape,
KeyCode::Char('r'),
KeyCode::Char('b'),
] {
let mut form = ConnectionForm {
keymap: super::super::keymap::BmuxKeyMap::from_config(
&bcode_config::TuiConfig::default(),
),
presentation: Presentation::Guided,
providers: Vec::new(),
picker: None,
selected: 0,
query: String::new(),
interactive: true,
device: Some(super::super::setup_device_login::DeviceLogin::failed_for_test()),
importing: false,
fields: std::array::from_fn(|_| {
TextInputState::new(TextEditBuffer::from_text(String::new()))
}),
focus: 4,
secret: zeroize::Zeroizing::new(String::new()),
review: false,
pending_declaration: None,
completed: EnrollmentStatus::Pending,
mode: EnrollmentMode::Configurable,
status: String::new(),
};
assert!(!form.handle_event(&Event::Key(bmux_keyboard::KeyStroke::simple(key))));
assert!(form.device.is_none());
assert_eq!(form.picker, Some(true));
}
}
}