use std::time::Instant;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::*;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Wrap};
use crate::ui::theme::ThemePalette;
use crate::app::model::app_state::AppState;
use crate::ui::primitives::atoms::key_chip;
use crate::ui::primitives::molecules::render_modal;
use crate::ui::primitives::utils::text_utils::wrapped_line_count;
pub struct ConnectionError;
impl ConnectionError {
pub fn render(frame: &mut Frame, state: &AppState, now: Instant, theme: &ThemePalette) {
Self::render_at(frame, state, now, theme);
}
pub fn render_at(frame: &mut Frame, state: &AppState, now: Instant, theme: &ThemePalette) {
let error_state = &state.connection_error;
let Some(ref error_info) = error_state.error_info else {
return;
};
let details_expanded = error_state.details_expanded;
let full_area = frame.area();
let modal_outer_width = full_area.width * 70 / 100;
let content_width = modal_outer_width.saturating_sub(4);
const FIXED_OVERHEAD: u16 = 8;
let details_height = if details_expanded {
let detail_text = error_state.masked_details().unwrap_or("");
let tab_replaced = detail_text.replace('\t', " ");
let detail_header = 1u16;
detail_header + wrapped_line_count(&tab_replaced, content_width)
} else {
1
};
let terminal_cap = full_area.height.saturating_sub(2);
let max_height = if details_expanded {
(full_area.height * 60 / 100).min(terminal_cap).max(9)
} else {
terminal_cap.max(9)
};
let height = Constraint::Length((FIXED_OVERHEAD + details_height).clamp(9, max_height));
let hint_text = " Esc to close ";
let (_, inner) = render_modal(
frame,
Constraint::Percentage(70),
height,
" Connection Error ",
hint_text,
theme,
);
let chunks = Layout::vertical([
Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(1), Constraint::Length(1), Constraint::Length(1), ])
.split(inner);
Self::render_summary(frame, chunks[0], error_info.kind.summary(), theme);
Self::render_hint(frame, chunks[2], state, theme);
Self::render_details_section(frame, chunks[4], error_state, details_expanded, theme);
Self::render_actions(frame, chunks[6], state, now, theme);
}
fn render_summary(frame: &mut Frame, area: Rect, summary: &str, theme: &ThemePalette) {
let line = Line::from(vec![
Span::styled("✗ ", Style::default().fg(theme.semantic.status.error)),
Span::styled(
summary,
Style::default()
.fg(theme.semantic.status.error)
.add_modifier(Modifier::BOLD),
),
]);
frame.render_widget(Paragraph::new(line), area);
}
fn render_hint(frame: &mut Frame, area: Rect, state: &AppState, theme: &ThemePalette) {
let hint = state
.connection_error
.error_info
.as_ref()
.map_or("", |e| e.kind.hint());
let mut spans = vec![
Span::styled("Hint: ", Style::default().fg(theme.semantic.text.accent)),
Span::styled(hint, Style::default().fg(theme.semantic.text.secondary)),
];
if state.session.is_service_connection()
&& let Some(ref path) = state.runtime.service_file_path
{
spans.push(Span::styled(
format!(" (edit {})", path.display()),
Style::default().fg(theme.semantic.text.muted),
));
}
let line = Line::from(spans);
frame.render_widget(Paragraph::new(line), area);
}
fn render_details_section(
frame: &mut Frame,
area: Rect,
error_state: &crate::app::model::connection::error_state::ConnectionErrorState,
expanded: bool,
theme: &ThemePalette,
) {
if expanded {
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(area);
let toggle_line = Line::from(vec![Span::styled(
"▼ Details",
Style::default()
.fg(theme.component.navigation.section_header)
.add_modifier(Modifier::BOLD),
)]);
frame.render_widget(Paragraph::new(toggle_line), chunks[0]);
if let Some(details) = error_state.masked_details() {
let lines: Vec<Line> = details
.lines()
.map(|l| Line::from(l.replace('\t', " ")))
.collect();
let scroll = error_state.scroll_offset;
let para = Paragraph::new(lines)
.scroll((scroll as u16, 0))
.wrap(Wrap { trim: false })
.style(Style::default().fg(theme.semantic.text.muted));
frame.render_widget(para, chunks[1]);
}
} else {
let toggle_line = Line::from(vec![
Span::styled(
"▶ ",
Style::default().fg(theme.component.navigation.section_header),
),
Span::styled(
"Details ",
Style::default().fg(theme.component.navigation.section_header),
),
Span::styled(
"(press d to expand)",
Style::default().fg(theme.semantic.text.muted),
),
]);
frame.render_widget(Paragraph::new(toggle_line), area);
}
}
fn render_actions(
frame: &mut Frame,
area: Rect,
state: &AppState,
now: Instant,
theme: &ThemePalette,
) {
let error_state = &state.connection_error;
let mut spans = vec![Span::styled(
"Actions: ",
Style::default().fg(theme.semantic.text.muted),
)];
if state.session.is_service_connection() {
spans.push(key_chip("r", theme));
spans.push(Span::raw(" Retry "));
} else {
spans.push(key_chip("e", theme));
spans.push(Span::raw(" Re-enter "));
}
spans.extend([
key_chip("s", theme),
Span::raw(" Switch "),
key_chip("d", theme),
Span::raw(" Details "),
key_chip("y", theme),
Span::raw(" Copy"),
]);
if error_state.is_copied_visible_at(now) {
spans.push(Span::raw(" "));
spans.push(Span::styled(
"Copied!",
Style::default()
.fg(theme.semantic.status.success)
.add_modifier(Modifier::BOLD),
));
}
let line = Line::from(spans);
frame.render_widget(Paragraph::new(line), area);
}
}