use std::time::Duration;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use ratatui::Frame;
use ratatui_notifications::{
Anchor, Animation, AutoDismiss, Level, Notification, Notifications, SlideDirection,
};
use crate::theme;
pub const LIST_CURSOR: &str = "❯ ";
pub const TOAST_TTL: Duration = Duration::from_secs(3);
pub fn list_cursor_style() -> Style {
Style::default()
.fg(theme::highlight())
.add_modifier(Modifier::BOLD)
}
pub fn checkbox(checked: bool) -> Span<'static> {
let (mark, color) = if checked {
("[x] ", theme::selected())
} else {
("[ ] ", theme::muted())
};
Span::styled(mark.to_string(), Style::default().fg(color))
}
pub fn item_title_style(checked: bool) -> Style {
if checked {
Style::default().fg(theme::selected())
} else {
Style::default().fg(theme::muted())
}
}
pub fn item_desc_style() -> Style {
Style::default()
.fg(theme::muted())
.add_modifier(Modifier::DIM)
}
pub fn status_icon(ok: bool) -> Span<'static> {
let (icon, color) = if ok {
("✓ ", theme::success())
} else {
("✗ ", theme::warning())
};
Span::styled(icon.to_string(), Style::default().fg(color))
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum ToastKind {
Info,
Success,
Warn,
Error,
}
pub fn push_toast(toasts: &mut Notifications, message: impl Into<String>, kind: ToastKind) {
let message = message.into();
if message.is_empty() {
return;
}
let (level, color) = match kind {
ToastKind::Info => (Level::Info, theme::accent()),
ToastKind::Success => (Level::Info, theme::success()),
ToastKind::Warn => (Level::Warn, theme::warning()),
ToastKind::Error => (Level::Error, theme::danger()),
};
let Ok(notif) = Notification::new(message)
.level(level)
.anchor(Anchor::TopRight)
.animation(Animation::Slide)
.slide_direction(SlideDirection::FromRight)
.auto_dismiss(AutoDismiss::After(TOAST_TTL))
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(color))
.title_style(Style::default().fg(color).add_modifier(Modifier::BOLD))
.build()
else {
return;
};
let _ = toasts.add(notif);
}
#[allow(dead_code)]
pub fn toast_info(toasts: &mut Notifications, message: impl Into<String>) {
push_toast(toasts, message, ToastKind::Info);
}
pub fn toast_ok(toasts: &mut Notifications, message: impl Into<String>) {
push_toast(toasts, message, ToastKind::Success);
}
pub fn toast_warn(toasts: &mut Notifications, message: impl Into<String>) {
push_toast(toasts, message, ToastKind::Warn);
}
pub fn toast_error(toasts: &mut Notifications, message: impl Into<String>) {
push_toast(toasts, message, ToastKind::Error);
}
pub fn tick_toasts(toasts: &mut Notifications, dt: Duration) {
toasts.tick(dt);
}
pub fn render_toasts(toasts: &mut Notifications, frame: &mut Frame<'_>, area: Rect) {
toasts.render(frame, area);
}
pub fn wrapping_index(current: usize, delta: isize, len: usize) -> Option<usize> {
if len == 0 {
return None;
}
let len_i = len as isize;
Some((current as isize + delta).rem_euclid(len_i) as usize)
}
pub fn input_paragraph(
value: &str,
placeholder: &str,
active: bool,
title: &str,
) -> Paragraph<'static> {
let cursor = Span::styled("▏".to_string(), Style::default().fg(theme::text()));
let mut spans: Vec<Span<'static>> = Vec::new();
if value.is_empty() {
if active {
spans.push(cursor);
}
spans.push(Span::styled(
placeholder.to_string(),
Style::default().fg(theme::muted()),
));
} else {
spans.push(Span::styled(
value.to_string(),
Style::default().fg(theme::text()),
));
if active {
spans.push(cursor);
}
}
let border = if active {
theme::accent()
} else {
theme::muted()
};
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(border))
.title(Span::styled(
title.to_string(),
Style::default()
.fg(theme::accent())
.add_modifier(Modifier::BOLD),
));
Paragraph::new(Line::from(spans)).block(block)
}
pub fn key_hint_line(items: &[(&str, &str)]) -> Line<'static> {
let key_style = Style::default()
.fg(theme::text())
.bg(theme::surface())
.add_modifier(Modifier::BOLD);
let label_style = Style::default().fg(theme::muted());
let mut spans: Vec<Span<'static>> = Vec::with_capacity(items.len() * 3);
for (index, (key, label)) in items.iter().enumerate() {
if index > 0 {
spans.push(Span::raw(" "));
}
spans.push(Span::styled(format!(" {key} "), key_style));
spans.push(Span::styled(format!(" {label}"), label_style));
}
Line::from(spans)
}