121 lines
3.6 KiB
Python
121 lines
3.6 KiB
Python
"""Provides classes for holding and managing notifications."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from time import time
|
|
from typing import Iterator
|
|
from uuid import uuid4
|
|
|
|
from rich.repr import Result
|
|
from typing_extensions import Literal, Self, TypeAlias
|
|
|
|
from textual.message import Message
|
|
|
|
SeverityLevel: TypeAlias = Literal["information", "warning", "error"]
|
|
"""The severity level for a notification."""
|
|
|
|
|
|
@dataclass
|
|
class Notify(Message, bubble=False):
|
|
"""Message to show a notification."""
|
|
|
|
notification: Notification
|
|
|
|
|
|
@dataclass
|
|
class Notification:
|
|
"""Holds the details of a notification."""
|
|
|
|
message: str
|
|
"""The message for the notification."""
|
|
|
|
title: str = ""
|
|
"""The title for the notification."""
|
|
|
|
severity: SeverityLevel = "information"
|
|
"""The severity level for the notification."""
|
|
|
|
timeout: float = 5
|
|
"""The timeout (in seconds) for the notification."""
|
|
|
|
markup: bool = False
|
|
"""Render the notification message as content markup?"""
|
|
|
|
raised_at: float = field(default_factory=time)
|
|
"""The time when the notification was raised (in Unix time)."""
|
|
|
|
identity: str = field(default_factory=lambda: str(uuid4()))
|
|
"""The unique identity of the notification."""
|
|
|
|
@property
|
|
def time_left(self) -> float:
|
|
"""The time left until this notification expires"""
|
|
return (self.raised_at + self.timeout) - time()
|
|
|
|
@property
|
|
def has_expired(self) -> bool:
|
|
"""Has the notification expired?"""
|
|
return self.time_left <= 0
|
|
|
|
def __rich_repr__(self) -> Result:
|
|
yield "message", self.message
|
|
yield "title", self.title, ""
|
|
yield "severity", self.severity
|
|
yield "raised_it", self.raised_at
|
|
yield "identity", self.identity
|
|
yield "time_left", self.time_left
|
|
yield "has_expired", self.has_expired
|
|
|
|
|
|
class Notifications:
|
|
"""Class for managing a collection of notifications."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialise the notification collection."""
|
|
self._notifications: dict[str, Notification] = {}
|
|
|
|
def _reap(self) -> Self:
|
|
"""Remove any expired notifications from the notification collection."""
|
|
for notification in list(self._notifications.values()):
|
|
if notification.has_expired:
|
|
del self._notifications[notification.identity]
|
|
return self
|
|
|
|
def add(self, notification: Notification) -> Self:
|
|
"""Add the given notification to the collection of managed notifications.
|
|
|
|
Args:
|
|
notification: The notification to add.
|
|
|
|
Returns:
|
|
Self.
|
|
"""
|
|
self._reap()._notifications[notification.identity] = notification
|
|
return self
|
|
|
|
def clear(self) -> Self:
|
|
"""Clear all the notifications."""
|
|
self._notifications.clear()
|
|
return self
|
|
|
|
def __len__(self) -> int:
|
|
"""The number of notifications."""
|
|
return len(self._reap()._notifications)
|
|
|
|
def __iter__(self) -> Iterator[Notification]:
|
|
return iter(self._reap()._notifications.values())
|
|
|
|
def __contains__(self, notification: Notification) -> bool:
|
|
return notification.identity in self._notifications
|
|
|
|
def __delitem__(self, notification: Notification) -> None:
|
|
try:
|
|
del self._reap()._notifications[notification.identity]
|
|
except KeyError:
|
|
# An attempt to remove a notification we don't know about is a
|
|
# no-op. What matters here is that the notification is forgotten
|
|
# about, and it looks like a caller has tried to be
|
|
# belt-and-braces. We're fine with this.
|
|
pass
|