use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph, Widget, Wrap},
};
use super::theme;
pub struct InfoPanel<'a> {
pub title: &'a str,
pub lines: Vec<Line<'a>>,
}
impl Widget for InfoPanel<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme::border_style())
.title(Span::styled(
format!(" {} ", self.title),
theme::heading_style(),
));
let inner = block.inner(area);
block.render(area, buf);
let paragraph = Paragraph::new(Text::from(self.lines))
.wrap(Wrap { trim: false })
.style(theme::body_style());
paragraph.render(inner, buf);
}
}
pub struct SelectableList<'a> {
pub title: &'a str,
pub items: &'a [SelectableItem],
pub selected: usize,
pub scroll_offset: usize,
}
pub struct SelectableItem {
pub label: String,
pub hint: String,
pub is_active: bool,
pub installed: bool,
}
impl Widget for SelectableList<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(theme::border_style())
.title(Span::styled(
format!(" {} ", self.title),
theme::heading_style(),
));
let inner = block.inner(area);
block.render(area, buf);
let visible_items = inner.height as usize;
let start = self.scroll_offset;
let end = (start + visible_items).min(self.items.len());
for (i, item) in self.items[start..end].iter().enumerate() {
let abs_idx = start + i;
let y = inner.y + u16::try_from(i).unwrap_or(u16::MAX);
if y >= inner.y + inner.height {
break;
}
let row_area = Rect::new(inner.x, y, inner.width, 1);
let is_cursor = abs_idx == self.selected;
let (marker, marker_style) = if is_cursor {
if item.is_active {
("\u{25cf} ", theme::accent_style()) } else {
("\u{203a} ", theme::selected_style()) }
} else if item.is_active {
("\u{25cf} ", theme::accent_style()) } else {
("\u{25cb} ", theme::unselected_style()) };
let label_style = if is_cursor {
theme::selected_style()
} else if item.installed {
theme::success_style()
} else {
theme::body_style()
};
let hint_style = if item.installed {
theme::success_style().add_modifier(Modifier::DIM)
} else {
theme::dim_style()
};
let mut spans = vec![
Span::styled(marker, marker_style),
Span::styled(&item.label, label_style),
];
if !item.hint.is_empty() {
spans.push(Span::raw(" "));
spans.push(Span::styled(format!("({})", item.hint), hint_style));
}
if item.installed && !is_cursor {
spans.push(Span::styled(" \u{2713}", theme::success_style()));
}
Paragraph::new(Line::from(spans)).render(row_area, buf);
}
if self.scroll_offset > 0 {
let indicator = Rect::new(inner.x + inner.width.saturating_sub(3), inner.y, 3, 1);
Paragraph::new(Span::styled(" \u{25b2}", theme::dim_style())).render(indicator, buf);
}
if end < self.items.len() {
let indicator = Rect::new(
inner.x + inner.width.saturating_sub(3),
inner.y + inner.height.saturating_sub(1),
3,
1,
);
Paragraph::new(Span::styled(" \u{25bc}", theme::dim_style())).render(indicator, buf);
}
}
}
pub struct StepIndicator<'a> {
pub current: u8,
pub total: u8,
pub label: &'a str,
pub status: StepStatus,
}
pub enum StepStatus {
Pending,
Active,
Complete,
Error,
}
impl Widget for StepIndicator<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let (icon, style) = match self.status {
StepStatus::Pending => (" ", theme::dim_style()),
StepStatus::Active => ("\u{25b6}", theme::heading_style()), StepStatus::Complete => ("\u{2713}", theme::success_style()), StepStatus::Error => ("\u{2717}", Style::default().fg(theme::ERR_RED)), };
let line = Line::from(vec![
Span::styled(
format!("[{}/{}] ", self.current, self.total),
theme::dim_style(),
),
Span::styled(format!("{icon} "), style),
Span::styled(self.label, style),
]);
Paragraph::new(line).render(area, buf);
}
}
pub struct Banner;
const BANNER_ART: &str = r"
███████╗███████╗██████╗ ██████╗ ██████╗██╗ █████╗ ██╗ ██╗
╚══███╔╝██╔════╝██╔══██╗██╔═══██╗██╔════╝██║ ██╔══██╗██║ ██║
███╔╝ █████╗ ██████╔╝██║ ██║██║ ██║ ███████║██║ █╗ ██║
███╔╝ ██╔══╝ ██╔══██╗██║ ██║██║ ██║ ██╔══██║██║███╗██║
███████╗███████╗██║ ██║╚██████╔╝╚██████╗███████╗██║ ██║╚███╔███╔╝
╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
";
impl Widget for Banner {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut lines: Vec<Line> = vec![Line::from("")];
for line in BANNER_ART.lines() {
if !line.is_empty() {
lines.push(Line::from(Span::styled(line, theme::title_style())));
}
}
lines.push(Line::from(Span::styled(
"\u{1f980} ZEROCLAW \u{1f980}",
theme::accent_style(),
)));
lines.push(Line::from(""));
Paragraph::new(lines)
.alignment(Alignment::Center)
.render(area, buf);
}
}
pub struct ConfirmedLine<'a> {
pub label: &'a str,
pub value: &'a str,
}
impl Widget for ConfirmedLine<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let line = Line::from(vec![
Span::styled("\u{25c7} ", theme::success_style()), Span::styled(self.label, theme::body_style()),
Span::raw(" "),
Span::styled(self.value, theme::heading_style()),
]);
Paragraph::new(line).render(area, buf);
}
}
pub struct InputPrompt<'a> {
pub label: &'a str,
pub input: &'a str,
pub masked: bool,
}
impl Widget for InputPrompt<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let display = if self.masked {
"\u{2022}".repeat(self.input.len()) } else {
self.input.to_string()
};
let line = Line::from(vec![
Span::styled("\u{25c6} ", theme::accent_style()), Span::styled(self.label, theme::heading_style()),
Span::raw(" "),
Span::styled(display, theme::input_style()),
Span::styled("\u{2588}", theme::accent_style()), ]);
Paragraph::new(line).render(area, buf);
}
}