use std::time::Duration;
use crate::console::{Console, Renderable};
use crate::panel::Panel;
use crate::progress_bar::ProgressBar;
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
use crate::utils::box_chars::{BoxChars, ROUNDED};
use crate::utils::padding::PaddingDimensions;
#[derive(Debug, Clone)]
pub enum ToastType {
Success,
Error,
Warning,
Info,
Custom(Style),
}
impl ToastType {
fn default_icon(&self) -> &'static str {
match self {
ToastType::Success => "✓",
ToastType::Error => "✗",
ToastType::Warning => "⚠",
ToastType::Info => "ℹ",
ToastType::Custom(_) => "•",
}
}
fn border_style(&self) -> Style {
match self {
ToastType::Success => Style::parse("green"),
ToastType::Error => Style::parse("red"),
ToastType::Warning => Style::parse("yellow"),
ToastType::Info => Style::parse("blue"),
ToastType::Custom(style) => style.clone(),
}
}
fn background_style(&self) -> Style {
match self {
ToastType::Success => Style::parse("on dark_green dim"),
ToastType::Error => Style::parse("on dark_red dim"),
ToastType::Warning => Style::parse("on dark_yellow dim"),
ToastType::Info => Style::parse("on dark_blue dim"),
ToastType::Custom(style) => style.background_style(),
}
}
fn icon_style(&self) -> Style {
match self {
ToastType::Success => Style::parse("bold green"),
ToastType::Error => Style::parse("bold red"),
ToastType::Warning => Style::parse("bold yellow"),
ToastType::Info => Style::parse("bold blue"),
ToastType::Custom(style) => style.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct Toast {
message: String,
toast_type: ToastType,
duration: Duration,
icon: Option<String>,
show_progress: bool,
width: Option<usize>,
box_chars: &'static BoxChars,
}
impl Toast {
pub fn new(message: impl Into<String>) -> Self {
Toast {
message: message.into(),
toast_type: ToastType::Info,
duration: Duration::from_secs(3),
icon: None,
show_progress: false,
width: None,
box_chars: &ROUNDED,
}
}
pub fn success(message: impl Into<String>) -> Self {
Toast::new(message).toast_type(ToastType::Success)
}
pub fn error(message: impl Into<String>) -> Self {
Toast::new(message).toast_type(ToastType::Error)
}
pub fn warning(message: impl Into<String>) -> Self {
Toast::new(message).toast_type(ToastType::Warning)
}
pub fn info(message: impl Into<String>) -> Self {
Toast::new(message).toast_type(ToastType::Info)
}
#[must_use]
pub fn toast_type(mut self, toast_type: ToastType) -> Self {
self.toast_type = toast_type;
self
}
#[must_use]
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
#[must_use]
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
#[must_use]
pub fn show_progress(mut self, show: bool) -> Self {
self.show_progress = show;
self
}
#[must_use]
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub fn box_chars(mut self, box_chars: &'static BoxChars) -> Self {
self.box_chars = box_chars;
self
}
fn get_icon(&self) -> &str {
self.icon
.as_deref()
.unwrap_or_else(|| self.toast_type.default_icon())
}
fn build_content(&self) -> Text {
let icon = self.get_icon();
let icon_style = self.toast_type.icon_style();
let mut content = Text::empty();
content.append_str("[", None);
content.append_str(icon, Some(icon_style));
content.append_str("] ", None);
content.append_str(&self.message, None);
content
}
fn build_panel(&self, elapsed: Option<Duration>) -> Panel {
let mut content = self.build_content();
if self.show_progress {
let progress_bar = self.build_progress_bar(elapsed);
content.append_str("\n", None);
let console = Console::builder().width(40).build();
let opts = console.options();
let segments = progress_bar.gilt_console(&console, &opts);
let mut pb_text = Text::empty();
for seg in &segments {
pb_text.append_str(&seg.text, seg.style().cloned());
}
content.append_text(&pb_text);
}
let border_style = self.toast_type.border_style();
let bg_style = self.toast_type.background_style();
let mut panel = Panel::new(content)
.with_box_chars(self.box_chars)
.with_border_style(border_style)
.with_style(bg_style)
.with_expand(false)
.with_padding(PaddingDimensions::Pair(0, 1));
if let Some(width) = self.width {
panel = panel.with_width(width);
}
panel
}
fn build_progress_bar(&self, elapsed: Option<Duration>) -> ProgressBar {
let total_secs = self.duration.as_secs_f64();
let elapsed_secs = elapsed.map(|d| d.as_secs_f64()).unwrap_or(0.0);
let completed = elapsed_secs.min(total_secs);
let mut bar = ProgressBar::new()
.with_total(Some(total_secs))
.with_completed(completed)
.with_width(Some(40));
match self.toast_type {
ToastType::Success => {
bar = bar.with_complete_style("green").with_style("dim green");
}
ToastType::Error => {
bar = bar.with_complete_style("red").with_style("dim red");
}
ToastType::Warning => {
bar = bar.with_complete_style("yellow").with_style("dim yellow");
}
ToastType::Info => {
bar = bar.with_complete_style("blue").with_style("dim blue");
}
ToastType::Custom(_) => {
bar = bar.with_complete_style("bold").with_style("dim");
}
}
bar
}
pub fn show(&self, console: &mut Console) {
let panel = self.build_panel(None);
console.print(&panel);
}
pub fn show_blocking(&self, console: &mut Console) {
self.show(console);
std::thread::sleep(self.duration);
}
}
impl Renderable for Toast {
fn gilt_console(
&self,
console: &Console,
_options: &crate::console::ConsoleOptions,
) -> Vec<Segment> {
let panel = self.build_panel(None);
panel.gilt_console(console, _options)
}
}
#[derive(Debug, Clone)]
pub struct ToastManager {
toasts: Vec<Toast>,
max_visible: usize,
}
impl ToastManager {
pub fn new() -> Self {
ToastManager {
toasts: Vec::new(),
max_visible: 3,
}
}
#[must_use]
pub fn max_visible(mut self, max: usize) -> Self {
self.max_visible = max;
self
}
pub fn push(&mut self, toast: Toast) {
self.toasts.push(toast);
}
pub fn show_all(&self, console: &mut Console) {
for toast in self.toasts.iter().take(self.max_visible) {
toast.show(console);
}
}
pub fn clear(&mut self) {
self.toasts.clear();
}
pub fn is_empty(&self) -> bool {
self.toasts.is_empty()
}
pub fn len(&self) -> usize {
self.toasts.len()
}
pub fn toasts(&self) -> &[Toast] {
&self.toasts
}
}
impl Default for ToastManager {
fn default() -> Self {
Self::new()
}
}
pub fn toast_success(message: impl Into<String>) {
crate::with_console(|console| {
Toast::success(message).show(console);
});
}
pub fn toast_error(message: impl Into<String>) {
crate::with_console(|console| {
Toast::error(message).show(console);
});
}
pub fn toast_warning(message: impl Into<String>) {
crate::with_console(|console| {
Toast::warning(message).show(console);
});
}
pub fn toast_info(message: impl Into<String>) {
crate::with_console(|console| {
Toast::info(message).show(console);
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::Console;
fn make_console() -> Console {
Console::builder()
.width(80)
.force_terminal(true)
.no_color(true)
.build()
}
#[test]
fn test_default_construction() {
let toast = Toast::new("Hello");
assert_eq!(toast.message, "Hello");
assert!(matches!(toast.toast_type, ToastType::Info));
assert_eq!(toast.duration, Duration::from_secs(3));
assert!(toast.icon.is_none());
assert!(!toast.show_progress);
}
#[test]
fn test_convenience_constructors() {
let success = Toast::success("Done");
assert!(matches!(success.toast_type, ToastType::Success));
assert_eq!(success.get_icon(), "✓");
let error = Toast::error("Failed");
assert!(matches!(error.toast_type, ToastType::Error));
assert_eq!(error.get_icon(), "✗");
let warning = Toast::warning("Caution");
assert!(matches!(warning.toast_type, ToastType::Warning));
assert_eq!(warning.get_icon(), "⚠");
let info = Toast::info("Note");
assert!(matches!(info.toast_type, ToastType::Info));
assert_eq!(info.get_icon(), "ℹ");
}
#[test]
fn test_builder_type() {
let toast = Toast::new("Test").toast_type(ToastType::Success);
assert!(matches!(toast.toast_type, ToastType::Success));
}
#[test]
fn test_builder_duration() {
let toast = Toast::new("Test").duration(Duration::from_secs(10));
assert_eq!(toast.duration, Duration::from_secs(10));
}
#[test]
fn test_builder_icon() {
let toast = Toast::new("Test").icon("🎉");
assert_eq!(toast.icon, Some("🎉".to_string()));
assert_eq!(toast.get_icon(), "🎉");
}
#[test]
fn test_builder_show_progress() {
let toast = Toast::new("Test").show_progress(true);
assert!(toast.show_progress);
}
#[test]
fn test_builder_width() {
let toast = Toast::new("Test").width(60);
assert_eq!(toast.width, Some(60));
}
#[test]
fn test_builder_chain() {
let toast = Toast::new("Test")
.toast_type(ToastType::Warning)
.duration(Duration::from_secs(5))
.icon("⚡")
.show_progress(true)
.width(50);
assert!(matches!(toast.toast_type, ToastType::Warning));
assert_eq!(toast.duration, Duration::from_secs(5));
assert_eq!(toast.icon, Some("⚡".to_string()));
assert!(toast.show_progress);
assert_eq!(toast.width, Some(50));
}
#[test]
fn test_toast_type_default_icons() {
assert_eq!(ToastType::Success.default_icon(), "✓");
assert_eq!(ToastType::Error.default_icon(), "✗");
assert_eq!(ToastType::Warning.default_icon(), "⚠");
assert_eq!(ToastType::Info.default_icon(), "ℹ");
assert_eq!(ToastType::Custom(Style::null()).default_icon(), "•");
}
#[test]
fn test_toast_type_border_styles() {
let _ = ToastType::Success.border_style();
let _ = ToastType::Error.border_style();
let _ = ToastType::Warning.border_style();
let _ = ToastType::Info.border_style();
let _ = ToastType::Custom(Style::null()).border_style();
}
#[test]
fn test_build_content() {
let toast = Toast::success("Operation completed");
let content = toast.build_content();
let plain = content.plain();
assert!(plain.contains("✓"));
assert!(plain.contains("Operation completed"));
assert!(plain.contains("[✓]"));
}
#[test]
fn test_build_content_custom_icon() {
let toast = Toast::info("Message").icon("🚀");
let content = toast.build_content();
let plain = content.plain();
assert!(plain.contains("🚀"));
assert!(!plain.contains("ℹ"));
}
#[test]
fn test_show() {
let mut console = make_console();
console.begin_capture();
let toast = Toast::success("Done!");
toast.show(&mut console);
let output = console.end_capture();
assert!(output.contains("Done!"));
assert!(output.contains("✓"));
}
#[test]
fn test_renderable() {
let console = make_console();
let opts = console.options();
let toast = Toast::info("Test message");
let segments = toast.gilt_console(&console, &opts);
assert!(!segments.is_empty());
}
#[test]
fn test_manager_default() {
let manager = ToastManager::new();
assert!(manager.is_empty());
assert_eq!(manager.max_visible, 3);
}
#[test]
fn test_manager_max_visible() {
let manager = ToastManager::new().max_visible(5);
assert_eq!(manager.max_visible, 5);
}
#[test]
fn test_manager_push() {
let mut manager = ToastManager::new();
manager.push(Toast::success("Done"));
assert_eq!(manager.len(), 1);
assert!(!manager.is_empty());
}
#[test]
fn test_manager_push_multiple() {
let mut manager = ToastManager::new();
manager.push(Toast::success("1"));
manager.push(Toast::info("2"));
manager.push(Toast::warning("3"));
assert_eq!(manager.len(), 3);
}
#[test]
fn test_manager_clear() {
let mut manager = ToastManager::new();
manager.push(Toast::success("Done"));
manager.clear();
assert!(manager.is_empty());
assert_eq!(manager.len(), 0);
}
#[test]
fn test_manager_show_all() {
let mut console = make_console();
console.begin_capture();
let mut manager = ToastManager::new();
manager.push(Toast::success("First"));
manager.push(Toast::info("Second"));
manager.show_all(&mut console);
let output = console.end_capture();
assert!(output.contains("First"));
assert!(output.contains("Second"));
}
#[test]
fn test_manager_max_visible_limit() {
let mut console = make_console();
console.begin_capture();
let mut manager = ToastManager::new().max_visible(2);
manager.push(Toast::success("1"));
manager.push(Toast::info("2"));
manager.push(Toast::warning("3"));
manager.show_all(&mut console);
let output = console.end_capture();
assert!(output.contains('1'));
assert!(output.contains('2'));
}
#[test]
fn test_manager_toasts_accessor() {
let mut manager = ToastManager::new();
manager.push(Toast::success("Test"));
assert_eq!(manager.toasts().len(), 1);
}
#[test]
fn test_empty_message() {
let toast = Toast::success("");
assert_eq!(toast.message, "");
let content = toast.build_content();
assert!(content.plain().contains("✓"));
}
#[test]
fn test_long_message() {
let long_msg = "a".repeat(200);
let toast = Toast::info(&long_msg);
assert_eq!(toast.message.len(), 200);
}
#[test]
fn test_multiline_message() {
let toast = Toast::info("Line 1\nLine 2");
let content = toast.build_content();
let plain = content.plain();
assert!(plain.contains("Line 1"));
assert!(plain.contains("Line 2"));
}
#[test]
fn test_custom_style() {
let style = Style::parse("magenta bold");
let toast = Toast::new("Custom").toast_type(ToastType::Custom(style));
assert!(matches!(toast.toast_type, ToastType::Custom(_)));
}
#[test]
fn test_zero_duration() {
let toast = Toast::new("Quick").duration(Duration::from_secs(0));
assert_eq!(toast.duration, Duration::from_secs(0));
}
#[test]
fn test_progress_bar_construction() {
let toast = Toast::info("Progress").show_progress(true);
let bar = toast.build_progress_bar(Some(Duration::from_secs(1)));
assert!(bar.total.is_some());
assert_eq!(bar.total.unwrap(), 3.0); }
}