Implemented Phase 10 (Desktop Notifications) with three stages: notify-rust integration, smart filtering, and production polish. Stage 1 - Base Implementation: - Add NotificationManager module (src/notifications.rs, 350+ lines) - Integrate notify-rust 4.11 with feature flag "notifications" - Implement NotificationsConfig in config.toml (enabled, only_mentions, show_preview) - Add notification_manager field to TdClient - Create configure_notifications() method for config integration - Hook into handle_new_message_update() to send notifications - Send notifications for messages outside current chat - Format notification body with sender name and message preview Stage 2 - Smart Filtering: - Sync muted chats from Telegram (sync_muted_chats method) - Filter muted chats from notifications automatically - Add MessageInfo::has_mention() to detect @username mentions - Implement only_mentions filter (notify only when mentioned) - Beautify media labels with emojis (📷 📹 🎤 🎨 📎 etc.) - Support 10+ media types in notification preview Stage 3 - Production Polish: - Add graceful error handling (no panics on notification failure) - Implement comprehensive logging (tracing::debug!/warn!) - Add timeout_ms configuration (0 = system default) - Add urgency configuration (low/normal/critical, Linux only) - Platform-specific #[cfg] for urgency support - Log all notification skip reasons at debug level Hotkey Change: - Move profile view from 'i' to Ctrl+i to avoid conflicts Technical Details: - Cross-platform support (macOS, Linux, Windows) - Feature flag for optional notifications support - Graceful fallback when notifications unavailable - LRU-friendly muted chats sync - Test coverage for all core notification logic - All 75 tests passing Files Changed: - NEW: src/notifications.rs - Complete NotificationManager - NEW: config.example.toml - Example configuration with notifications - Modified: Cargo.toml - Add notify-rust 4.11 dependency - Modified: src/config/mod.rs - Add NotificationsConfig struct - Modified: src/tdlib/types.rs - Add has_mention() method - Modified: src/tdlib/client.rs - Add notification integration - Modified: src/tdlib/update_handlers.rs - Hook notifications - Modified: src/config/keybindings.rs - Change profile to Ctrl+i - Modified: tests/* - Add notification config to tests Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
643 lines
21 KiB
Rust
643 lines
21 KiB
Rust
pub mod keybindings;
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use std::fs;
|
||
use std::path::PathBuf;
|
||
|
||
pub use keybindings::{Command, Keybindings};
|
||
|
||
/// Главная конфигурация приложения.
|
||
///
|
||
/// Загружается из `~/.config/tele-tui/config.toml` и содержит настройки
|
||
/// общего поведения, цветовой схемы и горячих клавиш.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```ignore
|
||
/// // Загрузка конфигурации
|
||
/// let config = Config::load();
|
||
///
|
||
/// // Доступ к настройкам
|
||
/// println!("Timezone: {}", config.general.timezone);
|
||
/// println!("Incoming color: {}", config.colors.incoming_message);
|
||
/// ```
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct Config {
|
||
/// Общие настройки (timezone и т.д.).
|
||
#[serde(default)]
|
||
pub general: GeneralConfig,
|
||
|
||
/// Цветовая схема интерфейса.
|
||
#[serde(default)]
|
||
pub colors: ColorsConfig,
|
||
|
||
/// Горячие клавиши.
|
||
#[serde(default)]
|
||
pub keybindings: Keybindings,
|
||
|
||
/// Настройки desktop notifications.
|
||
#[serde(default)]
|
||
pub notifications: NotificationsConfig,
|
||
}
|
||
|
||
/// Общие настройки приложения.
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct GeneralConfig {
|
||
/// Часовой пояс в формате "+03:00" или "-05:00"
|
||
#[serde(default = "default_timezone")]
|
||
pub timezone: String,
|
||
}
|
||
|
||
/// Цветовая схема интерфейса.
|
||
///
|
||
/// Поддерживаемые цвета: red, green, blue, yellow, cyan, magenta,
|
||
/// white, black, gray/grey, а также light-варианты (lightred, lightgreen и т.д.).
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ColorsConfig {
|
||
/// Цвет входящих сообщений (white, gray, cyan и т.д.)
|
||
#[serde(default = "default_incoming_color")]
|
||
pub incoming_message: String,
|
||
|
||
/// Цвет исходящих сообщений
|
||
#[serde(default = "default_outgoing_color")]
|
||
pub outgoing_message: String,
|
||
|
||
/// Цвет выбранного сообщения
|
||
#[serde(default = "default_selected_color")]
|
||
pub selected_message: String,
|
||
|
||
/// Цвет своих реакций
|
||
#[serde(default = "default_reaction_chosen_color")]
|
||
pub reaction_chosen: String,
|
||
|
||
/// Цвет чужих реакций
|
||
#[serde(default = "default_reaction_other_color")]
|
||
pub reaction_other: String,
|
||
}
|
||
|
||
/// Настройки desktop notifications.
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct NotificationsConfig {
|
||
/// Включить/выключить уведомления
|
||
#[serde(default = "default_notifications_enabled")]
|
||
pub enabled: bool,
|
||
|
||
/// Уведомлять только при @упоминаниях
|
||
#[serde(default)]
|
||
pub only_mentions: bool,
|
||
|
||
/// Показывать превью текста сообщения
|
||
#[serde(default = "default_show_preview")]
|
||
pub show_preview: bool,
|
||
|
||
/// Продолжительность показа уведомления (миллисекунды)
|
||
/// 0 = системное значение по умолчанию
|
||
#[serde(default = "default_notification_timeout")]
|
||
pub timeout_ms: i32,
|
||
|
||
/// Уровень важности: "low", "normal", "critical"
|
||
#[serde(default = "default_notification_urgency")]
|
||
pub urgency: String,
|
||
}
|
||
|
||
// Дефолтные значения
|
||
fn default_timezone() -> String {
|
||
"+03:00".to_string()
|
||
}
|
||
|
||
fn default_incoming_color() -> String {
|
||
"white".to_string()
|
||
}
|
||
|
||
fn default_outgoing_color() -> String {
|
||
"green".to_string()
|
||
}
|
||
|
||
fn default_selected_color() -> String {
|
||
"yellow".to_string()
|
||
}
|
||
|
||
fn default_reaction_chosen_color() -> String {
|
||
"yellow".to_string()
|
||
}
|
||
|
||
fn default_reaction_other_color() -> String {
|
||
"gray".to_string()
|
||
}
|
||
|
||
fn default_notifications_enabled() -> bool {
|
||
true
|
||
}
|
||
|
||
fn default_show_preview() -> bool {
|
||
true
|
||
}
|
||
|
||
fn default_notification_timeout() -> i32 {
|
||
5000 // 5 seconds
|
||
}
|
||
|
||
fn default_notification_urgency() -> String {
|
||
"normal".to_string()
|
||
}
|
||
|
||
impl Default for GeneralConfig {
|
||
fn default() -> Self {
|
||
Self { timezone: default_timezone() }
|
||
}
|
||
}
|
||
|
||
impl Default for ColorsConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
incoming_message: default_incoming_color(),
|
||
outgoing_message: default_outgoing_color(),
|
||
selected_message: default_selected_color(),
|
||
reaction_chosen: default_reaction_chosen_color(),
|
||
reaction_other: default_reaction_other_color(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for NotificationsConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
enabled: default_notifications_enabled(),
|
||
only_mentions: false,
|
||
show_preview: default_show_preview(),
|
||
timeout_ms: default_notification_timeout(),
|
||
urgency: default_notification_urgency(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for Config {
|
||
fn default() -> Self {
|
||
Self {
|
||
general: GeneralConfig::default(),
|
||
colors: ColorsConfig::default(),
|
||
keybindings: Keybindings::default(),
|
||
notifications: NotificationsConfig::default(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Config {
|
||
/// Валидация конфигурации
|
||
pub fn validate(&self) -> Result<(), String> {
|
||
// Проверка timezone
|
||
if !self.general.timezone.starts_with('+') && !self.general.timezone.starts_with('-') {
|
||
return Err(format!(
|
||
"Invalid timezone (must start with + or -): {}",
|
||
self.general.timezone
|
||
));
|
||
}
|
||
|
||
// Проверка цветов
|
||
let valid_colors = [
|
||
"black",
|
||
"red",
|
||
"green",
|
||
"yellow",
|
||
"blue",
|
||
"magenta",
|
||
"cyan",
|
||
"gray",
|
||
"grey",
|
||
"white",
|
||
"darkgray",
|
||
"darkgrey",
|
||
"lightred",
|
||
"lightgreen",
|
||
"lightyellow",
|
||
"lightblue",
|
||
"lightmagenta",
|
||
"lightcyan",
|
||
];
|
||
|
||
for color_name in [
|
||
&self.colors.incoming_message,
|
||
&self.colors.outgoing_message,
|
||
&self.colors.selected_message,
|
||
&self.colors.reaction_chosen,
|
||
&self.colors.reaction_other,
|
||
] {
|
||
if !valid_colors.contains(&color_name.to_lowercase().as_str()) {
|
||
return Err(format!("Invalid color: {}", color_name));
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Возвращает путь к конфигурационному файлу.
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// `Some(PathBuf)` - `~/.config/tele-tui/config.toml`
|
||
/// `None` - Не удалось определить директорию конфигурации
|
||
pub fn config_path() -> Option<PathBuf> {
|
||
dirs::config_dir().map(|mut path| {
|
||
path.push("tele-tui");
|
||
path.push("config.toml");
|
||
path
|
||
})
|
||
}
|
||
|
||
/// Путь к директории конфигурации
|
||
pub fn config_dir() -> Option<PathBuf> {
|
||
dirs::config_dir().map(|mut path| {
|
||
path.push("tele-tui");
|
||
path
|
||
})
|
||
}
|
||
|
||
/// Загружает конфигурацию из файла.
|
||
///
|
||
/// Ищет конфиг в `~/.config/tele-tui/config.toml`.
|
||
/// Если файл не существует, создаёт дефолтный.
|
||
/// Если файл невалиден, возвращает дефолтные значения.
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// Всегда возвращает валидную конфигурацию.
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```ignore
|
||
/// let config = Config::load();
|
||
/// ```
|
||
pub fn load() -> Self {
|
||
let config_path = match Self::config_path() {
|
||
Some(path) => path,
|
||
None => {
|
||
tracing::warn!("Could not determine config directory, using defaults");
|
||
return Self::default();
|
||
}
|
||
};
|
||
|
||
if !config_path.exists() {
|
||
// Создаём дефолтный конфиг при первом запуске
|
||
let default_config = Self::default();
|
||
if let Err(e) = default_config.save() {
|
||
tracing::warn!("Could not create default config: {}", e);
|
||
}
|
||
return default_config;
|
||
}
|
||
|
||
match fs::read_to_string(&config_path) {
|
||
Ok(content) => match toml::from_str::<Config>(&content) {
|
||
Ok(config) => {
|
||
// Валидируем загруженный конфиг
|
||
if let Err(e) = config.validate() {
|
||
tracing::error!("Config validation error: {}", e);
|
||
tracing::warn!("Using default configuration instead");
|
||
Self::default()
|
||
} else {
|
||
config
|
||
}
|
||
}
|
||
Err(e) => {
|
||
tracing::warn!("Could not parse config file: {}", e);
|
||
Self::default()
|
||
}
|
||
},
|
||
Err(e) => {
|
||
tracing::warn!("Could not read config file: {}", e);
|
||
Self::default()
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Сохраняет конфигурацию в файл.
|
||
///
|
||
/// Создаёт директорию `~/.config/tele-tui/` если её нет.
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// * `Ok(())` - Конфиг сохранен
|
||
/// * `Err(String)` - Ошибка сохранения
|
||
pub fn save(&self) -> Result<(), String> {
|
||
let config_dir =
|
||
Self::config_dir().ok_or_else(|| "Could not determine config directory".to_string())?;
|
||
|
||
// Создаём директорию если её нет
|
||
fs::create_dir_all(&config_dir)
|
||
.map_err(|e| format!("Could not create config directory: {}", e))?;
|
||
|
||
let config_path = config_dir.join("config.toml");
|
||
|
||
let toml_string = toml::to_string_pretty(self)
|
||
.map_err(|e| format!("Could not serialize config: {}", e))?;
|
||
|
||
fs::write(&config_path, toml_string)
|
||
.map_err(|e| format!("Could not write config file: {}", e))?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Парсит строку цвета в `ratatui::style::Color`.
|
||
///
|
||
/// Поддерживает стандартные цвета (red, green, blue и т.д.),
|
||
/// light-варианты (lightred, lightgreen и т.д.) и grey/gray.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `color_str` - Название цвета (case-insensitive)
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// `Color` - Соответствующий цвет или `White` если цвет не распознан
|
||
///
|
||
/// # Examples
|
||
///
|
||
/// ```ignore
|
||
/// let color = config.parse_color("red");
|
||
/// let color = config.parse_color("LightBlue");
|
||
/// ```
|
||
pub fn parse_color(&self, color_str: &str) -> ratatui::style::Color {
|
||
use ratatui::style::Color;
|
||
|
||
match color_str.to_lowercase().as_str() {
|
||
"black" => Color::Black,
|
||
"red" => Color::Red,
|
||
"green" => Color::Green,
|
||
"yellow" => Color::Yellow,
|
||
"blue" => Color::Blue,
|
||
"magenta" => Color::Magenta,
|
||
"cyan" => Color::Cyan,
|
||
"gray" | "grey" => Color::Gray,
|
||
"white" => Color::White,
|
||
"darkgray" | "darkgrey" => Color::DarkGray,
|
||
"lightred" => Color::LightRed,
|
||
"lightgreen" => Color::LightGreen,
|
||
"lightyellow" => Color::LightYellow,
|
||
"lightblue" => Color::LightBlue,
|
||
"lightmagenta" => Color::LightMagenta,
|
||
"lightcyan" => Color::LightCyan,
|
||
_ => Color::White, // fallback
|
||
}
|
||
}
|
||
|
||
/// Путь к файлу credentials
|
||
pub fn credentials_path() -> Option<PathBuf> {
|
||
Self::config_dir().map(|dir| dir.join("credentials"))
|
||
}
|
||
|
||
/// Загружает API_ID и API_HASH для Telegram.
|
||
///
|
||
/// Ищет credentials в следующем порядке:
|
||
/// 1. `~/.config/tele-tui/credentials` файл
|
||
/// 2. Переменные окружения `API_ID` и `API_HASH`
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// * `Ok((api_id, api_hash))` - Учетные данные найдены
|
||
/// * `Err(String)` - Ошибка с инструкциями по настройке
|
||
///
|
||
/// # Credentials Format
|
||
///
|
||
/// Файл `~/.config/tele-tui/credentials`:
|
||
/// ```text
|
||
/// API_ID=12345
|
||
/// API_HASH=your_api_hash_here
|
||
/// ```
|
||
pub fn load_credentials() -> Result<(i32, String), String> {
|
||
// 1. Пробуем загрузить из ~/.config/tele-tui/credentials
|
||
if let Some(credentials) = Self::load_credentials_from_file() {
|
||
return Ok(credentials);
|
||
}
|
||
|
||
// 2. Пробуем загрузить из переменных окружения (.env)
|
||
if let Some(credentials) = Self::load_credentials_from_env() {
|
||
return Ok(credentials);
|
||
}
|
||
|
||
// 3. Не нашли credentials - возвращаем инструкции
|
||
let credentials_path = Self::credentials_path()
|
||
.map(|p| p.display().to_string())
|
||
.unwrap_or_else(|| "~/.config/tele-tui/credentials".to_string());
|
||
|
||
Err(format!(
|
||
"Telegram API credentials not found!\n\n\
|
||
Please create a file at:\n {}\n\n\
|
||
With the following content:\n\
|
||
API_ID=your_api_id\n\
|
||
API_HASH=your_api_hash\n\n\
|
||
You can get API credentials at: https://my.telegram.org/apps\n\n\
|
||
Alternatively, you can create a .env file in the current directory.",
|
||
credentials_path
|
||
))
|
||
}
|
||
|
||
/// Загружает credentials из файла ~/.config/tele-tui/credentials
|
||
fn load_credentials_from_file() -> Option<(i32, String)> {
|
||
let cred_path = Self::credentials_path()?;
|
||
|
||
if !cred_path.exists() {
|
||
return None;
|
||
}
|
||
|
||
let content = fs::read_to_string(&cred_path).ok()?;
|
||
let mut api_id: Option<i32> = None;
|
||
let mut api_hash: Option<String> = None;
|
||
|
||
for line in content.lines() {
|
||
let line = line.trim();
|
||
if line.is_empty() || line.starts_with('#') {
|
||
continue;
|
||
}
|
||
|
||
let (key, value) = line.split_once('=')?;
|
||
let key = key.trim();
|
||
let value = value.trim();
|
||
|
||
match key {
|
||
"API_ID" => api_id = value.parse().ok(),
|
||
"API_HASH" => api_hash = Some(value.to_string()),
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
Some((api_id?, api_hash?))
|
||
}
|
||
|
||
/// Загружает credentials из переменных окружения (.env)
|
||
fn load_credentials_from_env() -> Option<(i32, String)> {
|
||
use std::env;
|
||
|
||
let api_id_str = env::var("API_ID").ok()?;
|
||
let api_hash = env::var("API_HASH").ok()?;
|
||
let api_id = api_id_str.parse::<i32>().ok()?;
|
||
|
||
Some((api_id, api_hash))
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||
|
||
#[test]
|
||
fn test_config_default_includes_keybindings() {
|
||
let config = Config::default();
|
||
let keybindings = &config.keybindings;
|
||
|
||
// Test that keybindings exist for common commands
|
||
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)) == Some(Command::ReplyMessage));
|
||
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('к'), KeyModifiers::NONE)) == Some(Command::ReplyMessage));
|
||
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE)) == Some(Command::ForwardMessage));
|
||
assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('а'), KeyModifiers::NONE)) == Some(Command::ForwardMessage));
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_valid() {
|
||
let config = Config::default();
|
||
assert!(config.validate().is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_invalid_timezone_no_sign() {
|
||
let mut config = Config::default();
|
||
config.general.timezone = "03:00".to_string();
|
||
|
||
let result = config.validate();
|
||
assert!(result.is_err());
|
||
assert!(result.unwrap_err().contains("timezone"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_valid_negative_timezone() {
|
||
let mut config = Config::default();
|
||
config.general.timezone = "-05:00".to_string();
|
||
|
||
assert!(config.validate().is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_valid_positive_timezone() {
|
||
let mut config = Config::default();
|
||
config.general.timezone = "+09:00".to_string();
|
||
|
||
assert!(config.validate().is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_invalid_color_incoming() {
|
||
let mut config = Config::default();
|
||
config.colors.incoming_message = "rainbow".to_string();
|
||
|
||
let result = config.validate();
|
||
assert!(result.is_err());
|
||
assert!(result.unwrap_err().contains("Invalid color"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_invalid_color_outgoing() {
|
||
let mut config = Config::default();
|
||
config.colors.outgoing_message = "purple".to_string();
|
||
|
||
let result = config.validate();
|
||
assert!(result.is_err());
|
||
assert!(result.unwrap_err().contains("Invalid color"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_invalid_color_selected() {
|
||
let mut config = Config::default();
|
||
config.colors.selected_message = "pink".to_string();
|
||
|
||
let result = config.validate();
|
||
assert!(result.is_err());
|
||
assert!(result.unwrap_err().contains("Invalid color"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_valid_all_standard_colors() {
|
||
let colors = [
|
||
"black", "red", "green", "yellow", "blue", "magenta",
|
||
"cyan", "gray", "grey", "white", "darkgray", "darkgrey",
|
||
"lightred", "lightgreen", "lightyellow", "lightblue",
|
||
"lightmagenta", "lightcyan"
|
||
];
|
||
|
||
for color in colors {
|
||
let mut config = Config::default();
|
||
config.colors.incoming_message = color.to_string();
|
||
config.colors.outgoing_message = color.to_string();
|
||
config.colors.selected_message = color.to_string();
|
||
config.colors.reaction_chosen = color.to_string();
|
||
config.colors.reaction_other = color.to_string();
|
||
|
||
assert!(
|
||
config.validate().is_ok(),
|
||
"Color '{}' should be valid",
|
||
color
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_config_validate_case_insensitive_colors() {
|
||
let mut config = Config::default();
|
||
config.colors.incoming_message = "RED".to_string();
|
||
config.colors.outgoing_message = "Green".to_string();
|
||
config.colors.selected_message = "YELLOW".to_string();
|
||
|
||
assert!(config.validate().is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_color_standard() {
|
||
let config = Config::default();
|
||
|
||
use ratatui::style::Color;
|
||
assert_eq!(config.parse_color("red"), Color::Red);
|
||
assert_eq!(config.parse_color("green"), Color::Green);
|
||
assert_eq!(config.parse_color("blue"), Color::Blue);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_color_light_variants() {
|
||
let config = Config::default();
|
||
|
||
use ratatui::style::Color;
|
||
assert_eq!(config.parse_color("lightred"), Color::LightRed);
|
||
assert_eq!(config.parse_color("lightgreen"), Color::LightGreen);
|
||
assert_eq!(config.parse_color("lightblue"), Color::LightBlue);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_color_gray_variants() {
|
||
let config = Config::default();
|
||
|
||
use ratatui::style::Color;
|
||
assert_eq!(config.parse_color("gray"), Color::Gray);
|
||
assert_eq!(config.parse_color("grey"), Color::Gray);
|
||
assert_eq!(config.parse_color("darkgray"), Color::DarkGray);
|
||
assert_eq!(config.parse_color("darkgrey"), Color::DarkGray);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_color_case_insensitive() {
|
||
let config = Config::default();
|
||
|
||
use ratatui::style::Color;
|
||
assert_eq!(config.parse_color("RED"), Color::Red);
|
||
assert_eq!(config.parse_color("Green"), Color::Green);
|
||
assert_eq!(config.parse_color("LIGHTBLUE"), Color::LightBlue);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_color_invalid_fallback() {
|
||
let config = Config::default();
|
||
|
||
use ratatui::style::Color;
|
||
// Invalid colors should fallback to White
|
||
assert_eq!(config.parse_color("rainbow"), Color::White);
|
||
assert_eq!(config.parse_color("purple"), Color::White);
|
||
assert_eq!(config.parse_color("unknown"), Color::White);
|
||
}
|
||
}
|