use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusToastLevel {
Info,
Success,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct StatusToast {
pub text: String,
pub level: StatusToastLevel,
pub created_at: Instant,
pub ttl_ms: Option<u64>,
}
impl StatusToast {
#[must_use]
pub fn new(text: impl Into<String>, level: StatusToastLevel, ttl_ms: Option<u64>) -> Self {
Self {
text: text.into(),
level,
created_at: Instant::now(),
ttl_ms,
}
}
#[must_use]
pub fn is_expired(&self, now: Instant) -> bool {
self.ttl_ms
.is_some_and(|ttl| now.duration_since(self.created_at).as_millis() >= u128::from(ttl))
}
}
impl App {
pub fn push_status_toast(
&mut self,
text: impl Into<String>,
level: StatusToastLevel,
ttl_ms: Option<u64>,
) {
let toast = StatusToast::new(text, level, ttl_ms);
self.status_toasts.push_back(toast);
while self.status_toasts.len() > 24 {
self.status_toasts.pop_front();
}
self.needs_redraw = true;
}
pub const STICKY_ERROR_TTL_MS: u64 = 8_000;
pub fn set_sticky_status(
&mut self,
text: impl Into<String>,
level: StatusToastLevel,
ttl_ms: Option<u64>,
) {
let ttl_ms = match level {
StatusToastLevel::Error => Some(
ttl_ms
.unwrap_or(Self::STICKY_ERROR_TTL_MS)
.min(Self::STICKY_ERROR_TTL_MS),
),
_ => ttl_ms,
};
self.sticky_status = Some(StatusToast::new(text, level, ttl_ms));
self.needs_redraw = true;
}
pub fn clear_sticky_status(&mut self) {
if self.sticky_status.take().is_some() {
self.needs_redraw = true;
}
}
pub fn acknowledge_sticky_on_composer_activity(&mut self) {
if self
.sticky_status
.as_ref()
.is_some_and(|toast| matches!(toast.level, StatusToastLevel::Error))
{
self.clear_sticky_status();
}
}
pub(super) fn classify_status_text(text: &str) -> (StatusToastLevel, Option<u64>, bool) {
let lower = text.to_ascii_lowercase();
let has = |needle: &str| lower.contains(needle);
if has("offline mode") || has("context critical") {
return (StatusToastLevel::Warning, None, true);
}
if has("error")
|| has("failed")
|| has("denied")
|| has("timeout")
|| has("aborted")
|| has("critical")
{
return (
StatusToastLevel::Error,
Some(Self::STICKY_ERROR_TTL_MS),
true,
);
}
let negated = has("not ")
|| has("no longer")
|| has("no ")
|| has("could not")
|| has("couldn't")
|| has("cannot")
|| has("can't")
|| has("unable");
if !negated
&& (has("saved")
|| has("loaded")
|| has("queued")
|| has("found")
|| has("enabled")
|| has("completed"))
{
return (StatusToastLevel::Success, Some(5_000), false);
}
if has("cancelled") || has("canceled") || has("warning") {
return (StatusToastLevel::Warning, Some(5_000), false);
}
(StatusToastLevel::Info, Some(4_000), false)
}
fn is_mode_switch_status_message(message: &str) -> bool {
message.starts_with("Switched to ") && message.ends_with(" mode")
}
pub fn sync_status_message_to_toasts(&mut self) {
let current = self.status_message.clone();
if self.last_status_message_seen == current {
return;
}
self.last_status_message_seen = current.clone();
let Some(message) = current else {
return;
};
if message.trim().is_empty() {
return;
}
if Self::is_mode_switch_status_message(&message) {
return;
}
let (level, ttl_ms, sticky) = Self::classify_status_text(&message);
if sticky {
self.set_sticky_status(message, level, ttl_ms);
} else {
if matches!(level, StatusToastLevel::Success)
&& self
.sticky_status
.as_ref()
.is_some_and(|toast| matches!(toast.level, StatusToastLevel::Error))
{
self.clear_sticky_status();
}
self.push_status_toast(message, level, ttl_ms);
}
}
fn prune_expired_status_toasts(&mut self, now: Instant) {
let queued_before = self.status_toasts.len();
self.status_toasts.retain(|toast| !toast.is_expired(now));
let queued_removed = self.status_toasts.len() != queued_before;
let sticky_removed = self
.sticky_status
.as_ref()
.is_some_and(|toast| toast.is_expired(now));
if sticky_removed {
self.sticky_status = None;
}
if queued_removed || sticky_removed {
self.needs_redraw = true;
}
}
pub fn active_status_toast(&mut self) -> Option<StatusToast> {
self.sync_status_message_to_toasts();
let now = Instant::now();
self.prune_expired_status_toasts(now);
self.sticky_status
.clone()
.or_else(|| self.status_toasts.back().cloned())
}
}