use super::{
ACTIVE_LOGIN_MODAL, CONFIRMATION_MODAL, CUSTOM_PROVIDER_REPLACEMENT_MODAL,
CUSTOM_PROVIDER_SETUP_MODAL, PICKER_MODAL, draw_modal_panel, picker_modal_list_area,
};
use crate::tui::{
PADDED_INLINE_SEPARATOR,
state::{AuthPickerMode, CustomProviderSetupStep, MissionControlState},
theme::MATRIX_GREEN,
};
use ratatui::{
Frame,
layout::{Rect, Size},
text::{Line, Span},
widgets::{List, ListItem, ListState, Paragraph, Wrap},
};
use tui_scrollview::{ScrollView, ScrollbarVisibility};
pub(crate) fn auth_picker_list_area(area: Rect, has_notice: bool) -> Option<Rect> {
let header_rows = 1 + u16::from(has_notice);
picker_modal_list_area(area, "Login Providers", header_rows)
}
pub(in crate::tui::render) fn draw_login_picker(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
let Some(picker) = &state.modals.login_picker else {
return;
};
let title = if picker.mode == AuthPickerMode::Logout {
"Logout Providers"
} else {
"Login Providers"
};
let Some(text_area) = draw_modal_panel(frame, area, title, PICKER_MODAL) else {
return;
};
if text_area.height == 0 {
return;
}
let help = if picker.mode == AuthPickerMode::Logout {
"[Enter] remove local auth [Esc] cancel"
} else {
"[Enter] connect [Esc] cancel"
};
frame.render_widget(
Paragraph::new(super::super::hotkey_line(help, MATRIX_GREEN.muted(false)))
.style(MATRIX_GREEN.muted(false)),
Rect::new(text_area.x, text_area.y, text_area.width, 1),
);
let mut list_y = text_area.y.saturating_add(1);
if let Some(notice) = &picker.notice
&& list_y < text_area.y.saturating_add(text_area.height)
{
frame.render_widget(
Paragraph::new(notice.clone()).style(MATRIX_GREEN.muted(false)),
Rect::new(text_area.x, list_y, text_area.width, 1),
);
list_y = list_y.saturating_add(1);
}
let list_height = text_area
.y
.saturating_add(text_area.height)
.saturating_sub(list_y);
if list_height == 0 {
return;
}
let content_height =
super::super::usize_to_u16_saturating(picker.entries.len().max(usize::from(list_height)));
let content_width =
super::super::scroll_view_content_width(Rect::new(0, 0, text_area.width, list_height));
let mut view = ScrollView::new(Size::new(content_width, content_height))
.vertical_scrollbar_visibility(ScrollbarVisibility::Always)
.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
let items = picker
.entries
.iter()
.map(|entry| {
ListItem::new(Line::from(vec![Span::raw(format!(
"{} ({}) [{}]{PADDED_INLINE_SEPARATOR}{}",
entry.label, entry.id, entry.status, entry.description
))]))
})
.collect::<Vec<_>>();
let mut list_state = ListState::default().with_selected(Some(picker.selected));
view.render_stateful_widget(
List::new(items)
.style(MATRIX_GREEN.pane())
.highlight_style(MATRIX_GREEN.selection())
.highlight_symbol("> "),
Rect::new(0, 0, content_width, content_height),
&mut list_state,
);
frame.render_stateful_widget(
view,
Rect::new(text_area.x, list_y, text_area.width, list_height),
&mut *state.scroll_views.login_picker.borrow_mut(),
);
}
pub(in crate::tui::render) fn draw_logout_confirmation(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
let Some(confirmation) = &state.modals.logout_confirmation else {
return;
};
let Some(text_area) = draw_modal_panel(frame, area, "Confirm Logout", CONFIRMATION_MODAL)
else {
return;
};
let lines = vec![
Line::from(Span::raw(format!(
"Remove local auth for {} ({})?",
confirmation.label, confirmation.provider_id
))),
Line::from(Span::raw("")),
super::super::hotkey_line(
"[Enter] remove local auth [Esc] cancel",
MATRIX_GREEN.pane(),
),
];
frame.render_widget(
Paragraph::new(lines)
.style(MATRIX_GREEN.pane())
.wrap(Wrap { trim: false }),
text_area,
);
}
pub(in crate::tui::render) fn draw_custom_provider_replacement_confirmation(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
let Some(confirmation) = &state.modals.custom_provider_replacement_confirmation else {
return;
};
let Some(text_area) = draw_modal_panel(
frame,
area,
"Replace Custom Provider",
CUSTOM_PROVIDER_REPLACEMENT_MODAL,
) else {
return;
};
let lines = vec![
Line::from(Span::raw(format!(
"Replace {} ({}) with new metadata?",
confirmation.existing_label, confirmation.provider_id
))),
Line::from(Span::raw("")),
Line::from(Span::raw(format!("New label: {}", confirmation.label))),
Line::from(Span::raw(format!(
"New base URL: {}",
confirmation.base_url
))),
Line::from(Span::raw(format!(
"API key: {}",
confirmation
.api_key_env_var
.as_deref()
.unwrap_or("not required")
))),
Line::from(Span::raw("")),
super::super::hotkey_line("[Enter] replace [Esc] cancel", MATRIX_GREEN.pane()),
];
frame.render_widget(
Paragraph::new(lines)
.style(MATRIX_GREEN.pane())
.wrap(Wrap { trim: false }),
text_area,
);
}
pub(in crate::tui::render) fn draw_custom_provider_setup(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
let Some(setup) = &state.modals.custom_provider_setup else {
return;
};
let Some(text_area) = draw_modal_panel(
frame,
area,
"Set up custom provider",
CUSTOM_PROVIDER_SETUP_MODAL,
) else {
return;
};
if text_area.height == 0 {
return;
}
let (step_count, step_label, prompt, help, input) = match setup.step {
CustomProviderSetupStep::Label => (
"1/3",
"Provider label",
"Label",
"Type a readable name. Provider ID is derived automatically.",
setup.label.as_str(),
),
CustomProviderSetupStep::BaseUrl => (
"2/3",
"Base URL",
"Base URL",
"Use an API root URL (e.g. /v1, /v4). https:// is required except localhost http://.",
setup.base_url.as_str(),
),
CustomProviderSetupStep::ApiKeyEnvVar => (
"3/3",
"API key environment variable",
"Env var",
"Optional. Leave blank if this provider does not require an API key.",
setup.api_key_env_var.as_str(),
),
};
let preview = setup
.provider_id
.as_deref()
.map(|id| format!("Provider ID: {id}"))
.unwrap_or_else(|| "Provider ID: derived after valid label".to_string());
let api_mode = if setup.step == CustomProviderSetupStep::ApiKeyEnvVar
&& setup.api_key_env_var.trim().is_empty()
{
"Auth mode: API key not required; no Authorization header will be sent."
} else {
"Auth mode: env-var backed when a variable name is provided."
};
let mut lines = vec![
Line::from(Span::raw(format!("Step {step_count} {step_label}"))),
Line::from(Span::raw("")),
Line::from(Span::raw(format!("{prompt}: {input}"))),
Line::from(Span::raw(help)),
Line::from(Span::raw(preview)),
];
if matches!(setup.step, CustomProviderSetupStep::ApiKeyEnvVar) {
lines.push(Line::from(Span::raw(api_mode)));
}
if let Some(error) = &setup.error {
lines.push(Line::from(Span::raw("")));
lines.push(Line::from(Span::raw(format!("Error: {error}"))));
}
lines.extend([
Line::from(Span::raw("")),
super::super::hotkey_line("[Enter] continue/save [Esc] cancel", MATRIX_GREEN.pane()),
Line::from(Span::raw(
"Saved config stores metadata only; never enter an API-key value.",
)),
]);
frame.render_widget(
Paragraph::new(lines)
.style(MATRIX_GREEN.pane())
.wrap(Wrap { trim: false }),
text_area,
);
}
pub(in crate::tui::render) fn draw_active_login(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
let Some(login) = &state.modals.active_login else {
return;
};
let Some(text_area) = draw_modal_panel(frame, area, "OpenAI Codex Login", ACTIVE_LOGIN_MODAL)
else {
return;
};
if text_area.height == 0 {
return;
}
let fallback_mask = if login.fallback_input.is_empty() {
"<paste redirect URL or authorization code, then [Enter]>".to_string()
} else {
"<redacted input staged; press [Enter] to submit>".to_string()
};
let mut lines = vec![super::super::hotkey_line(
"[Ctrl-Y] copy URL [Ctrl-O] open browser [Esc] cancel",
MATRIX_GREEN.pane(),
)];
if login.url.is_empty() {
lines.extend(
login
.instructions
.lines()
.map(|line| Line::from(Span::raw(line.to_string()))),
);
} else {
lines.extend([
super::super::hotkey_line(
"Open this URL in your browser, or use [Ctrl-O].",
MATRIX_GREEN.pane(),
),
Line::from(Span::raw(
"If needed, paste the redirect URL or authorization code below.",
)),
]);
lines.push(Line::from(Span::raw("")));
lines.extend(wrapped_prefixed_lines(
"URL: ",
&login.url,
text_area.width.max(1),
));
}
lines.extend([
Line::from(Span::raw("")),
Line::from(Span::raw(
"Manual fallback input is transient and redacted.",
)),
super::super::hotkey_line(fallback_mask, MATRIX_GREEN.pane()),
super::super::hotkey_line(
"[Enter] submit fallback [Esc] cancel without writing credentials",
MATRIX_GREEN.pane(),
),
]);
frame.render_widget(
Paragraph::new(lines)
.style(MATRIX_GREEN.pane())
.wrap(Wrap { trim: false }),
text_area,
);
}
fn wrapped_prefixed_lines(prefix: &str, text: &str, width: u16) -> Vec<Line<'static>> {
let width = width.max(1) as usize;
let prefix_len = prefix.chars().count();
let first_chunk_len = width.saturating_sub(prefix_len).max(1);
let mut chunks = text.chars();
let mut lines = Vec::new();
let first = chunks.by_ref().take(first_chunk_len).collect::<String>();
lines.push(Line::from(Span::raw(format!("{prefix}{first}"))));
loop {
let chunk = chunks.by_ref().take(width).collect::<String>();
if chunk.is_empty() {
break;
}
lines.push(Line::from(Span::raw(chunk)));
}
lines
}