use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget},
};
use crate::scroll_list::ScrollList;
use crate::popup::{Popup, PopupSize};
use crate::spin::SpinState;
use crate::spins::SpinVariant;
use crate::{
BorderType, BORDER_GRAY, CHECK, CROSS, OUTPUT_GRAY, POPUP_WIDTH, TEXT_PADDING,
};
fn truncate(text: &str, max_width: usize) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= max_width || max_width == 0 {
text.to_string()
} else if max_width >= 4 {
let truncated: String = chars[..max_width - 3].iter().collect();
format!("{}...", truncated)
} else {
chars[..max_width].iter().collect()
}
}
#[derive(Clone)]
pub struct SpinPopup<'a> {
popup: Popup<'a>,
variant: SpinVariant,
title: Option<&'a str>,
command: Option<&'a str>,
output_lines: Vec<&'a str>,
finished_msg: Option<&'a str>,
max_output_rows: u16,
spinner_style: Style,
command_style: Style,
output_style: Style,
}
impl<'a> SpinPopup<'a> {
pub fn new() -> Self {
Self {
popup: Popup::new(Color::White)
.padding(0)
.border_type(BorderType::Rounded),
variant: SpinVariant::default(),
title: None,
command: None,
output_lines: vec![],
finished_msg: Some("Presione Enter para continuar"),
max_output_rows: 10,
spinner_style: Style::new().fg(Color::Blue),
command_style: Style::new().fg(Color::White),
output_style: Style::new().fg(OUTPUT_GRAY),
}
}
pub fn title(mut self, title: &'a str) -> Self {
self.title = Some(title);
self
}
pub fn command(mut self, command: &'a str) -> Self {
self.command = Some(command);
self
}
pub fn output_lines(mut self, lines: &'a [&'a str]) -> Self {
self.output_lines = lines.to_vec();
self
}
pub fn variant(mut self, variant: SpinVariant) -> Self {
self.variant = variant;
self
}
pub fn finished_msg(mut self, msg: &'a str) -> Self {
self.finished_msg = Some(msg);
self
}
pub fn no_finished_msg(mut self) -> Self {
self.finished_msg = None;
self
}
pub fn max_output_rows(mut self, rows: u16) -> Self {
self.max_output_rows = rows;
self
}
pub fn spinner_style(mut self, style: Style) -> Self {
self.spinner_style = style;
self
}
pub fn command_style(mut self, style: Style) -> Self {
self.command_style = style;
self
}
pub fn output_style(mut self, style: Style) -> Self {
self.output_style = style;
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.popup = self.popup.border_color(color);
self
}
pub fn border_type(mut self, bt: BorderType) -> Self {
self.popup = self.popup.border_type(bt);
self
}
pub fn bg_color(mut self, color: Color) -> Self {
self.popup = self.popup.bg_color(color);
self
}
pub fn width(mut self, w: PopupSize) -> Self {
self.popup = self.popup.width(w);
self
}
pub fn height(mut self, h: PopupSize) -> Self {
self.popup = self.popup.height(h);
self
}
pub fn position(mut self, x: u16, y: u16) -> Self {
self.popup = self.popup.position(x, y);
self
}
pub fn origin(mut self, x: u16, y: u16) -> Self {
self.popup = self.popup.origin(x, y);
self
}
pub fn header(mut self) -> Self {
self.popup = self.popup.header();
self
}
pub fn padding(mut self, p: u16) -> Self {
self.popup = self.popup.padding(p);
self
}
pub fn resolve_rect(&self, area: Rect) -> Rect {
self.popup.resolve_rect(area)
}
}
impl StatefulWidget for SpinPopup<'_> {
type State = SpinState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let show_footer = state.finished && self.finished_msg.is_some();
let content_h = {
let mut h = 2u16; if self.command.is_some() {
h += 1; }
if !self.output_lines.is_empty() {
h += self.output_lines.len().min(self.max_output_rows as usize) as u16;
h += 1; }
if show_footer {
h += 2; }
h
};
let total_height = content_h + 2; let mut popup = self.popup;
if popup.get_height() == PopupSize::Auto {
popup = popup.height(PopupSize::Fixed(total_height));
}
let inner = popup.render_inner(area, buf);
let frames = self.variant.frames();
let (ch, spin_style) = if state.finished {
let ok = state.exit_code.map_or(true, |c| c == 0);
(if ok { CHECK } else { CROSS }, Style::new().fg(if ok { Color::Green } else { Color::Red }))
} else {
let frame = if frames.is_empty() {
" "
} else {
frames[state.frame % frames.len()]
};
(frame, self.spinner_style)
};
let mut constraints: Vec<Constraint> = vec![Constraint::Length(2)];
if self.command.is_some() {
constraints.push(Constraint::Length(1));
}
if !self.output_lines.is_empty() {
constraints.push(Constraint::Length(
self.output_lines.len().min(self.max_output_rows as usize) as u16 + 1,
));
}
if show_footer {
constraints.push(Constraint::Length(2));
}
let chunks = Layout::vertical(constraints).split(inner);
let mut idx = 0usize;
let spin_block = Block::bordered()
.borders(Borders::BOTTOM)
.border_style(Style::new().fg(BORDER_GRAY))
.padding(TEXT_PADDING);
let spin_inner = spin_block.inner(chunks[idx]);
spin_block.render(chunks[idx], buf);
let mut title_spans = vec![Span::styled(ch.to_string(), spin_style)];
if let Some(title) = self.title {
let title_max = (spin_inner.width.saturating_sub(2)) as usize;
let display = truncate(title, title_max);
title_spans.push(Span::styled(
format!(" {}", display),
Style::new().fg(Color::White).bold(),
));
}
Paragraph::new(Line::from(title_spans)).render(spin_inner, buf);
idx += 1;
if self.command.is_some() {
let cmd_max = chunks[idx].width as usize;
Paragraph::new(Line::from(Span::styled(
truncate(self.command.unwrap(), cmd_max),
self.command_style,
)))
.alignment(Alignment::Center)
.render(chunks[idx], buf);
idx += 1;
}
if !self.output_lines.is_empty() {
let lines: Vec<String> = self.output_lines.into_iter().map(|s| s.to_string()).collect();
ScrollList::new(lines)
.style(self.output_style)
.borders(Borders::TOP)
.border_style(Style::new().fg(BORDER_GRAY))
.render(chunks[idx], buf, &mut state.scroll_list);
idx += 1;
}
if show_footer {
let footer_block = Block::bordered()
.borders(Borders::TOP)
.border_style(Style::new().fg(BORDER_GRAY))
.padding(TEXT_PADDING);
let footer_inner = footer_block.inner(chunks[idx]);
footer_block.render(chunks[idx], buf);
let footer_max = footer_inner.width as usize;
Paragraph::new(Line::from(Span::styled(
truncate(self.finished_msg.unwrap(), footer_max),
Style::new().fg(Color::Green),
)))
.alignment(Alignment::Center)
.render(footer_inner, buf);
}
}
}
#[cfg(test)]
mod tests;