use colored::Colorize;
use std::io;
use crate::error::Result;
use crate::frame::BoxFrame;
use crate::terminal::get_content_width;
pub const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
#[derive(Debug, Clone)]
pub struct WaitingModal {
pub title: String,
pub message: String,
pub steps: Vec<(String, bool)>,
pub max_width: u16,
pub shortcuts: Option<crate::shortcuts::Shortcuts>,
}
impl WaitingModal {
pub fn new(title: impl Into<String>, message: impl Into<String>) -> Self {
Self {
title: title.into(),
message: message.into(),
steps: Vec::new(),
max_width: 0,
shortcuts: None,
}
}
pub fn with_step(mut self, step: impl Into<String>, completed: bool) -> Self {
self.steps.push((step.into(), completed));
self
}
pub fn with_max_width(mut self, width: u16) -> Self {
self.max_width = width;
self
}
pub fn with_shortcuts(mut self, shortcuts: impl Into<crate::shortcuts::Shortcuts>) -> Self {
self.shortcuts = Some(shortcuts.into());
self
}
pub fn render_spinner(&self, spinner_idx: usize, stdout: &mut io::Stdout) -> Result<()> {
let width = get_content_width(self.max_width);
let spinner_char = SPINNER_FRAMES[spinner_idx % SPINNER_FRAMES.len()];
let mut frame = BoxFrame::new(width);
frame.title = Some((self.title.clone(), false));
let main_line = format!(
"{} {}",
spinner_char.cyan().bold(),
self.message.white().bold()
);
frame.row(main_line);
if !self.steps.is_empty() {
frame.empty_row();
for (step, completed) in &self.steps {
if *completed {
frame.row(format!(" {} {}", "✓".green().bold(), step));
} else {
frame.row(format!(" {} {}", "•".dimmed(), step.dimmed()));
}
}
}
if let Some(ref sc) = self.shortcuts {
frame.shortcuts(sc);
} else {
frame.footer("Please wait...".dimmed().to_string());
}
frame.render(stdout)
}
pub fn render_static(&self, stdout: &mut io::Stdout) -> Result<()> {
let width = get_content_width(self.max_width);
let mut frame = BoxFrame::new(width);
frame.title = Some((self.title.clone(), false));
frame.row(self.message.clone());
if !self.steps.is_empty() {
frame.empty_row();
for (step, completed) in &self.steps {
if *completed {
frame.row(format!(" {} {}", "✓".green().bold(), step));
} else {
frame.row(format!(" {} {}", "•".dimmed(), step.dimmed()));
}
}
}
if let Some(ref sc) = self.shortcuts {
frame.shortcuts(sc);
} else {
frame.footer("Please wait...".dimmed().to_string());
}
frame.render(stdout)
}
}
pub fn render_boxed_status<S: AsRef<str>>(title: &str, lines: &[S]) -> Result<()> {
let mut stdout = io::stdout();
let width = get_content_width(80);
let mut frame = BoxFrame::new(width);
frame.title = Some((title.to_string(), false));
for line in lines {
frame.row(line.as_ref());
}
frame.render(&mut stdout)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_waiting_modal_builder() {
let modal = WaitingModal::new("RESTORING BACKUP", "Unpacking archive...")
.with_step("Verify hash", true)
.with_step("Decompressing tar.zst", false)
.with_max_width(90);
assert_eq!(modal.title, "RESTORING BACKUP");
assert_eq!(modal.message, "Unpacking archive...");
assert_eq!(modal.steps.len(), 2);
assert_eq!(modal.steps[0].0, "Verify hash");
assert!(modal.steps[0].1);
assert_eq!(modal.steps[1].0, "Decompressing tar.zst");
assert!(!modal.steps[1].1);
assert_eq!(modal.max_width, 90);
}
}