use ratatui::prelude::*;
use ratatui::widgets::{Block, Paragraph};
use crate::tui::theme::Theme;
use super::{PopupListCtx, render_search_bar, truncate_str};
pub(super) fn render_mcp_toggle(
ctx: &PopupListCtx<'_>,
buf: &mut Buffer,
block: Block,
items: &[crate::tui::state::McpToggleItem],
) {
let inner = block.inner(ctx.popup_area);
block.render(ctx.popup_area, buf);
let q = ctx.search.to_lowercase();
let filtered: Vec<&crate::tui::state::McpToggleItem> = items
.iter()
.filter(|it| {
it.name.to_lowercase().contains(&q) || it.description.to_lowercase().contains(&q)
})
.collect();
render_search_bar(
inner,
buf,
ctx.search,
filtered.len(),
items.len(),
ctx.theme,
);
let list_inner = Rect::new(
inner.x,
inner.y + 1,
inner.width,
inner.height.saturating_sub(1),
);
if filtered.is_empty() {
Paragraph::new(Span::styled(
" no matches",
Style::default().fg(ctx.theme.text_muted),
))
.render(
Rect::new(list_inner.x, list_inner.y, list_inner.width, 1),
buf,
);
return;
}
let skip = ctx.scroll as usize;
let mut row_y = list_inner.y;
for (i, item) in filtered.iter().enumerate() {
if i < skip {
continue;
}
if row_y >= list_inner.y + list_inner.height {
break;
}
let is_sel = i == ctx.selected;
let check = if item.enabled { "✓" } else { "✗" };
let check_color = if item.enabled {
ctx.theme.success
} else {
ctx.theme.error
};
let tool_info = if item.tool_count > 0 {
format!(" {} tools", item.tool_count)
} else {
String::new()
};
let name_width = 24usize;
let padded_name = format!("{:<width$}", item.name, width = name_width);
let mut spans = vec![
Span::styled(format!(" [{}] ", check), Style::default().fg(check_color)),
Span::styled(
padded_name,
Style::default()
.fg(if is_sel {
ctx.theme.accent
} else {
ctx.theme.text
})
.add_modifier(if is_sel {
Modifier::BOLD
} else {
Modifier::empty()
}),
),
Span::styled(
format!("{:<14}", item.status),
Style::default().fg(ctx.theme.text_dim),
),
];
if !tool_info.is_empty() {
spans.push(Span::styled(
tool_info,
Style::default().fg(ctx.theme.text_muted),
));
}
let line = Line::from(spans);
let row_area = Rect::new(list_inner.x, row_y, list_inner.width, 1);
if is_sel {
let bg_span = Span::styled(
" ".repeat(list_inner.width as usize),
Style::default().bg(ctx.theme.accent_dim),
);
Paragraph::new(Line::from(bg_span)).render(row_area, buf);
}
Paragraph::new(line).render(row_area, buf);
row_y += 1;
}
}
pub(super) fn render_session_resume(
ctx: &PopupListCtx<'_>,
buf: &mut Buffer,
block: Block,
items: &[(String, String, String, String)],
) {
let inner = block.inner(ctx.popup_area);
block.render(ctx.popup_area, buf);
let q = ctx.search.to_lowercase();
let filtered: Vec<&(String, String, String, String)> = items
.iter()
.filter(|(id, ts, st, task)| {
id.to_lowercase().contains(&q)
|| ts.to_lowercase().contains(&q)
|| st.to_lowercase().contains(&q)
|| task.to_lowercase().contains(&q)
})
.collect();
render_search_bar(
inner,
buf,
ctx.search,
filtered.len(),
items.len(),
ctx.theme,
);
let list_inner = Rect::new(
inner.x,
inner.y + 1,
inner.width,
inner.height.saturating_sub(1),
);
let id_w: u16 = 9;
let status_w: u16 = 8;
let time_w: u16 = 17;
let task_w = list_inner
.width
.saturating_sub(id_w + status_w + time_w + 3);
if filtered.is_empty() {
Paragraph::new(Span::styled(
" no matches",
Style::default().fg(ctx.theme.text_muted),
))
.render(
Rect::new(list_inner.x, list_inner.y, list_inner.width, 1),
buf,
);
return;
}
let skip = ctx.scroll as usize;
let mut row_y = list_inner.y;
for (i, (id, ts, status, task)) in filtered.iter().enumerate() {
if i < skip {
continue;
}
if row_y >= list_inner.y + list_inner.height {
break;
}
let is_selected = i == ctx.selected;
if is_selected {
let full = Rect::new(list_inner.x, row_y, list_inner.width, 1);
Paragraph::new(Span::styled(
" ".repeat(list_inner.width as usize),
Style::default().bg(ctx.theme.accent),
))
.render(full, buf);
}
let (fg_main, fg_dim, fg_status) = if is_selected {
(ctx.theme.bg, ctx.theme.bg, ctx.theme.bg)
} else {
(ctx.theme.text, ctx.theme.text_muted, ctx.theme.accent)
};
let bg = if is_selected {
ctx.theme.accent
} else {
ctx.theme.bg_surface
};
Paragraph::new(Span::styled(
format!(" {}", truncate_str(id, (id_w - 1) as usize)),
Style::default()
.fg(fg_main)
.bg(bg)
.add_modifier(if is_selected {
Modifier::BOLD
} else {
Modifier::empty()
}),
))
.render(Rect::new(list_inner.x, row_y, id_w, 1), buf);
Paragraph::new(Span::styled(
truncate_str(status, status_w as usize),
Style::default().fg(fg_status).bg(bg),
))
.render(Rect::new(list_inner.x + id_w, row_y, status_w, 1), buf);
Paragraph::new(Span::styled(
truncate_str(ts, time_w as usize),
Style::default().fg(fg_dim).bg(bg),
))
.render(
Rect::new(list_inner.x + id_w + status_w, row_y, time_w, 1),
buf,
);
if task_w > 0 {
Paragraph::new(Span::styled(
truncate_str(task, task_w as usize),
Style::default().fg(fg_dim).bg(bg),
))
.render(
Rect::new(list_inner.x + id_w + status_w + time_w, row_y, task_w, 1),
buf,
);
}
row_y += 1;
}
}
pub(super) fn render_table_select(
ctx: &PopupListCtx<'_>,
buf: &mut Buffer,
block: Block,
items: &[(String, String)],
) {
let inner = block.inner(ctx.popup_area);
block.render(ctx.popup_area, buf);
let q = ctx.search.to_lowercase();
let filtered: Vec<&(String, String)> = items
.iter()
.filter(|(n, d)| n.to_lowercase().contains(&q) || d.to_lowercase().contains(&q))
.collect();
render_search_bar(
inner,
buf,
ctx.search,
filtered.len(),
items.len(),
ctx.theme,
);
let list_inner = Rect::new(
inner.x,
inner.y + 1,
inner.width,
inner.height.saturating_sub(1),
);
let left_w = (list_inner.width as f32 * 0.35) as u16;
let right_w = list_inner.width.saturating_sub(left_w + 2);
let skip = ctx.scroll as usize;
let mut row_y = list_inner.y;
if filtered.is_empty() {
Paragraph::new(Span::styled(
" no matches",
Style::default().fg(ctx.theme.text_muted),
))
.render(
Rect::new(list_inner.x, list_inner.y, list_inner.width, 1),
buf,
);
return;
}
for (i, (name, desc)) in filtered.iter().enumerate() {
if i < skip {
continue;
}
if row_y >= list_inner.y + list_inner.height {
break;
}
let is_selected = i == ctx.selected;
let name_display = truncate_str(name, left_w as usize);
let left_area = Rect::new(list_inner.x, row_y, left_w, 1);
let name_style = if is_selected {
Style::default()
.fg(ctx.theme.bg)
.bg(ctx.theme.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(ctx.theme.accent)
};
if is_selected {
let full_row = Rect::new(list_inner.x, row_y, list_inner.width, 1);
Paragraph::new(Span::styled(
" ".repeat(list_inner.width as usize),
Style::default().bg(ctx.theme.accent),
))
.render(full_row, buf);
}
Paragraph::new(Span::styled(format!(" {name_display}"), name_style)).render(left_area, buf);
let gap_x = list_inner.x + left_w;
if gap_x < list_inner.x + list_inner.width {
let gap_area = Rect::new(gap_x, row_y, 2, 1);
let gap_style = if is_selected {
Style::default().bg(ctx.theme.accent)
} else {
Style::default()
};
Paragraph::new(Span::styled(" ", gap_style)).render(gap_area, buf);
}
let desc_display = truncate_str(desc, right_w as usize);
let right_x = list_inner.x + left_w + 2;
if right_x < list_inner.x + list_inner.width {
let right_area = Rect::new(right_x, row_y, right_w, 1);
let desc_style = if is_selected {
Style::default().fg(ctx.theme.bg).bg(ctx.theme.accent)
} else {
Style::default().fg(ctx.theme.text_muted)
};
Paragraph::new(Span::styled(desc_display, desc_style)).render(right_area, buf);
}
row_y += 1;
}
}
pub(super) fn render_config(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
items: &[crate::tui::state::ConfigItem],
selected: usize,
scroll: u16,
theme: &crate::tui::theme::Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let left_w = (inner.width as f32 * 0.60) as u16;
let right_w = inner.width.saturating_sub(left_w);
let skip = scroll as usize;
let mut row_y = inner.y;
for (i, item) in items.iter().enumerate() {
if i < skip {
continue;
}
if row_y >= inner.y + inner.height {
break;
}
let is_selected = i == selected;
let value_str = item.value.display();
let is_warned = item.warn_above.as_ref().is_some_and(|threshold| {
value_str.parse::<f64>().unwrap_or(0.0) > threshold.parse::<f64>().unwrap_or(f64::MAX)
});
let value_color = if is_warned {
theme.warning
} else {
match &item.value {
crate::tui::state::ConfigValue::Bool(true) => theme.success,
crate::tui::state::ConfigValue::Bool(false) => theme.text_muted,
crate::tui::state::ConfigValue::Choice { .. } => theme.accent,
crate::tui::state::ConfigValue::Text(_) => theme.text_muted,
}
};
let warn_suffix = if is_warned { " ⚠" } else { "" };
if is_selected {
let full_row = Rect::new(inner.x, row_y, inner.width, 1);
Paragraph::new(Span::styled(
" ".repeat(inner.width as usize),
Style::default().bg(theme.accent),
))
.render(full_row, buf);
let label_area = Rect::new(inner.x, row_y, left_w, 1);
let cursor = "❯ ";
Paragraph::new(Span::styled(
format!(
" {cursor}{}",
truncate_str(&item.label, (left_w as usize).saturating_sub(3))
),
Style::default()
.fg(theme.bg)
.bg(theme.accent)
.add_modifier(Modifier::BOLD),
))
.render(label_area, buf);
let right_x = inner.x + left_w;
if right_x < inner.x + inner.width {
let val_area = Rect::new(right_x, row_y, right_w, 1);
let val_display = format!("{value_str}{warn_suffix}");
let val_fg = if is_warned { theme.warning } else { theme.bg };
Paragraph::new(Span::styled(
truncate_str(&val_display, right_w as usize),
Style::default()
.fg(val_fg)
.bg(theme.accent)
.add_modifier(Modifier::BOLD),
))
.render(val_area, buf);
}
} else {
let label_area = Rect::new(inner.x, row_y, left_w, 1);
Paragraph::new(Span::styled(
format!(
" {}",
truncate_str(&item.label, (left_w as usize).saturating_sub(3))
),
Style::default().fg(theme.text),
))
.render(label_area, buf);
let right_x = inner.x + left_w;
if right_x < inner.x + inner.width {
let val_area = Rect::new(right_x, row_y, right_w, 1);
let val_display = format!("{value_str}{warn_suffix}");
Paragraph::new(Span::styled(
truncate_str(&val_display, right_w as usize),
Style::default().fg(value_color),
))
.render(val_area, buf);
}
}
row_y += 1;
}
}
pub(super) fn render_theme_select(
popup_area: Rect,
buf: &mut Buffer,
block: Block,
selected: usize,
dark_mode: bool,
scroll: u16,
theme: &crate::tui::theme::Theme,
) {
let inner = block.inner(popup_area);
block.render(popup_area, buf);
let families = Theme::families();
let skip = scroll as usize;
let mut row_y = inner.y;
for (i, family) in families.iter().enumerate() {
if i < skip {
continue;
}
if row_y >= inner.y + inner.height {
break;
}
let is_selected = i == selected;
if is_selected {
let full_row = Rect::new(inner.x, row_y, inner.width, 1);
Paragraph::new(Span::styled(
" ".repeat(inner.width as usize),
Style::default().bg(theme.accent),
))
.render(full_row, buf);
let dark_badge = if dark_mode {
" ◐ Dark "
} else {
" ◑ Dark "
};
let light_badge = if !dark_mode {
" ◑ Light "
} else {
" ◐ Light "
};
let has_light = family.light_id.is_some();
let name_max = inner.width.saturating_sub(20) as usize;
let name_display = truncate_str(family.name, name_max);
let name_span = Span::styled(
format!(" ❯ {}", name_display),
Style::default()
.fg(theme.bg)
.bg(theme.accent)
.add_modifier(Modifier::BOLD),
);
let dark_style = if dark_mode {
Style::default()
.fg(theme.accent)
.bg(theme.bg_surface)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.bg).bg(theme.accent)
};
let dark_span = Span::styled(dark_badge, dark_style);
let light_style = if !has_light {
Style::default().fg(theme.text_muted).bg(theme.accent)
} else if !dark_mode {
Style::default()
.fg(theme.accent)
.bg(theme.bg_surface)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.bg).bg(theme.accent)
};
let light_span = Span::styled(light_badge, light_style);
let name_area = Rect::new(inner.x, row_y, inner.width.saturating_sub(20), 1);
let badge_x = inner.x + inner.width.saturating_sub(19);
let badge_area = Rect::new(badge_x, row_y, 19, 1);
Paragraph::new(Line::from(vec![name_span])).render(name_area, buf);
Paragraph::new(Line::from(vec![dark_span, light_span])).render(badge_area, buf);
} else {
let name_max = inner.width.saturating_sub(2) as usize;
let name_display = truncate_str(family.name, name_max);
Paragraph::new(Span::styled(
format!(" {}", name_display),
Style::default().fg(theme.text_muted),
))
.render(Rect::new(inner.x, row_y, inner.width, 1), buf);
}
row_y += 1;
}
}