Split core and TUI crates
This commit is contained in:
282
crates/tele-tui/src/utils/formatting.rs
Normal file
282
crates/tele-tui/src/utils/formatting.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
#[cfg(test)]
|
||||
use chrono::FixedOffset;
|
||||
use chrono::{DateTime, Local, NaiveDate, Utc};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub trait LocalTimeSource {
|
||||
fn now_date(&self) -> NaiveDate;
|
||||
fn now_timestamp(&self) -> i32;
|
||||
fn format_timestamp(&self, timestamp: i32, format: &str) -> Option<String>;
|
||||
fn date_for_timestamp(&self, timestamp: i32) -> Option<NaiveDate>;
|
||||
}
|
||||
|
||||
pub struct SystemLocalTime;
|
||||
|
||||
impl LocalTimeSource for SystemLocalTime {
|
||||
fn now_date(&self) -> NaiveDate {
|
||||
Local::now().date_naive()
|
||||
}
|
||||
|
||||
fn now_timestamp(&self) -> i32 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i32
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, timestamp: i32, format: &str) -> Option<String> {
|
||||
DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
|
||||
.map(|dt| dt.with_timezone(&Local).format(format).to_string())
|
||||
}
|
||||
|
||||
fn date_for_timestamp(&self, timestamp: i32) -> Option<NaiveDate> {
|
||||
DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
|
||||
.map(|dt| dt.with_timezone(&Local).date_naive())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
pub struct FixedLocalTime {
|
||||
offset: FixedOffset,
|
||||
now: DateTime<FixedOffset>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl FixedLocalTime {
|
||||
fn new(offset: FixedOffset, now_timestamp: i32) -> Self {
|
||||
let now = DateTime::<Utc>::from_timestamp(now_timestamp as i64, 0)
|
||||
.expect("valid fixed timestamp")
|
||||
.with_timezone(&offset);
|
||||
Self { offset, now }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl LocalTimeSource for FixedLocalTime {
|
||||
fn now_date(&self) -> NaiveDate {
|
||||
self.now.date_naive()
|
||||
}
|
||||
|
||||
fn now_timestamp(&self) -> i32 {
|
||||
self.now.timestamp() as i32
|
||||
}
|
||||
|
||||
fn format_timestamp(&self, timestamp: i32, format: &str) -> Option<String> {
|
||||
DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
|
||||
.map(|dt| dt.with_timezone(&self.offset).format(format).to_string())
|
||||
}
|
||||
|
||||
fn date_for_timestamp(&self, timestamp: i32) -> Option<NaiveDate> {
|
||||
DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
|
||||
.map(|dt| dt.with_timezone(&self.offset).date_naive())
|
||||
}
|
||||
}
|
||||
|
||||
fn system_time() -> SystemLocalTime {
|
||||
SystemLocalTime
|
||||
}
|
||||
|
||||
/// Форматирование timestamp во время HH:MM в системной таймзоне.
|
||||
pub fn format_timestamp(timestamp: i32) -> String {
|
||||
format_timestamp_with(timestamp, &system_time())
|
||||
}
|
||||
|
||||
pub fn format_timestamp_with(timestamp: i32, time: &impl LocalTimeSource) -> String {
|
||||
time.format_timestamp(timestamp, "%H:%M")
|
||||
.unwrap_or_else(|| "00:00".to_string())
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в дату для разделителя.
|
||||
pub fn format_date(timestamp: i32) -> String {
|
||||
format_date_with(timestamp, &system_time())
|
||||
}
|
||||
|
||||
pub fn format_date_with(timestamp: i32, time: &impl LocalTimeSource) -> String {
|
||||
let Some(msg_day) = time.date_for_timestamp(timestamp) else {
|
||||
return "01.01.1970".to_string();
|
||||
};
|
||||
|
||||
let today = time.now_date();
|
||||
|
||||
if msg_day == today {
|
||||
"Сегодня".to_string()
|
||||
} else if Some(msg_day) == today.pred_opt() {
|
||||
"Вчера".to_string()
|
||||
} else {
|
||||
time.format_timestamp(timestamp, "%d.%m.%Y")
|
||||
.unwrap_or_else(|| "01.01.1970".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить день из timestamp для группировки.
|
||||
/// Возвращает число дней с 1970-01-01 в системной таймзоне.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_day(timestamp: i32) -> i64 {
|
||||
get_day_with(timestamp, &system_time())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_day_with(timestamp: i32, time: &impl LocalTimeSource) -> i64 {
|
||||
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch date");
|
||||
|
||||
time.date_for_timestamp(timestamp)
|
||||
.map(|date| date.signed_duration_since(epoch).num_days())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM) в системной таймзоне.
|
||||
pub fn format_datetime(timestamp: i32) -> String {
|
||||
format_datetime_with(timestamp, &system_time())
|
||||
}
|
||||
|
||||
pub fn format_datetime_with(timestamp: i32, time: &impl LocalTimeSource) -> String {
|
||||
time.format_timestamp(timestamp, "%d.%m.%Y %H:%M")
|
||||
.unwrap_or_else(|| "01.01.1970 00:00".to_string())
|
||||
}
|
||||
|
||||
/// Форматирование "был(а) онлайн" из timestamp
|
||||
pub fn format_was_online(timestamp: i32) -> String {
|
||||
format_was_online_with(timestamp, &system_time())
|
||||
}
|
||||
|
||||
pub fn format_was_online_with(timestamp: i32, time: &impl LocalTimeSource) -> String {
|
||||
let now = time.now_timestamp();
|
||||
let diff = now - timestamp;
|
||||
|
||||
if diff < 60 {
|
||||
"был(а) только что".to_string()
|
||||
} else if diff < 3600 {
|
||||
let mins = diff / 60;
|
||||
format!("был(а) {} мин. назад", mins)
|
||||
} else if diff < 86400 {
|
||||
let hours = diff / 3600;
|
||||
format!("был(а) {} ч. назад", hours)
|
||||
} else {
|
||||
// Показываем локальную дату
|
||||
let datetime = time
|
||||
.format_timestamp(timestamp, "%d.%m %H:%M")
|
||||
.unwrap_or_else(|| "давно".to_string());
|
||||
format!("был(а) {}", datetime)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fixed_time() -> FixedLocalTime {
|
||||
FixedLocalTime::new(
|
||||
FixedOffset::east_opt(3 * 3600).unwrap(),
|
||||
1_640_448_000, // 25.12.2021 03:00:00 +03:00
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_uses_supplied_timezone() {
|
||||
let timestamp = 1640000000;
|
||||
assert_eq!(format_timestamp_with(timestamp, &fixed_time()), "14:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_day() {
|
||||
let time = FixedLocalTime::new(FixedOffset::east_opt(0).unwrap(), 3 * 86_400);
|
||||
assert_eq!(get_day_with(0, &time), 0);
|
||||
assert_eq!(get_day_with(86400, &time), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_day_grouping() {
|
||||
let time = fixed_time();
|
||||
let msg1 = 1640000000;
|
||||
let msg2 = msg1 + 3600;
|
||||
assert_eq!(get_day_with(msg1, &time), get_day_with(msg2, &time));
|
||||
|
||||
let msg3 = msg1 + 172800;
|
||||
assert_ne!(get_day_with(msg1, &time), get_day_with(msg3, &time));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_datetime() {
|
||||
let timestamp = 1640000000;
|
||||
assert_eq!(format_datetime_with(timestamp, &fixed_time()), "20.12.2021 14:33");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_date_today() {
|
||||
let time = fixed_time();
|
||||
let result = format_date_with(time.now_timestamp(), &time);
|
||||
assert_eq!(result, "Сегодня");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_date_yesterday() {
|
||||
let time = fixed_time();
|
||||
let yesterday = time.now_timestamp() - 86400;
|
||||
let result = format_date_with(yesterday, &time);
|
||||
assert_eq!(result, "Вчера");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_date_old() {
|
||||
let old_timestamp = 1640000000;
|
||||
assert_eq!(format_date_with(old_timestamp, &fixed_time()), "20.12.2021");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_date_epoch() {
|
||||
let epoch = 0;
|
||||
let time = FixedLocalTime::new(FixedOffset::east_opt(0).unwrap(), 3 * 86_400);
|
||||
let result = format_date_with(epoch, &time);
|
||||
|
||||
assert!(result.contains('.'));
|
||||
assert!(result.contains("1970"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_was_online_just_now() {
|
||||
let time = fixed_time();
|
||||
let now = time.now_timestamp();
|
||||
let recent = now - 30;
|
||||
let result = format_was_online_with(recent, &time);
|
||||
assert_eq!(result, "был(а) только что");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_was_online_minutes_ago() {
|
||||
let time = fixed_time();
|
||||
let now = time.now_timestamp();
|
||||
let mins_ago = now - (15 * 60);
|
||||
let result = format_was_online_with(mins_ago, &time);
|
||||
assert_eq!(result, "был(а) 15 мин. назад");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_was_online_hours_ago() {
|
||||
let time = fixed_time();
|
||||
let now = time.now_timestamp();
|
||||
let hours_ago = now - (5 * 3600);
|
||||
let result = format_was_online_with(hours_ago, &time);
|
||||
assert_eq!(result, "был(а) 5 ч. назад");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_was_online_days_ago() {
|
||||
let time = fixed_time();
|
||||
let now = time.now_timestamp();
|
||||
let days_ago = now - (3 * 86400);
|
||||
let result = format_was_online_with(days_ago, &time);
|
||||
|
||||
assert!(result.starts_with("был(а)"));
|
||||
assert!(result.contains('.') || result.contains(':'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_was_online_very_old() {
|
||||
let old = 1577836800;
|
||||
let result = format_was_online_with(old, &fixed_time());
|
||||
|
||||
assert!(result.starts_with("был(а)"));
|
||||
assert!(result.contains('.'));
|
||||
}
|
||||
}
|
||||
11
crates/tele-tui/src/utils/mod.rs
Normal file
11
crates/tele-tui/src/utils/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod formatting;
|
||||
pub mod modal_handler;
|
||||
pub mod retry;
|
||||
pub mod tdlib;
|
||||
pub mod validation;
|
||||
|
||||
pub use formatting::*;
|
||||
// pub use modal_handler::*; // Используется через явный import
|
||||
pub use retry::{with_timeout, with_timeout_ignore, with_timeout_msg};
|
||||
pub use tdlib::*;
|
||||
pub use validation::*;
|
||||
86
crates/tele-tui/src/utils/modal_handler.rs
Normal file
86
crates/tele-tui/src/utils/modal_handler.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! Modal dialog utilities
|
||||
//!
|
||||
//! Переиспользуемая логика для обработки модальных окон (диалогов).
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
/// Обрабатывает клавиши для подтверждения Yes/No.
|
||||
///
|
||||
/// Поддерживает:
|
||||
/// - `y` / `Y` / `д` / `Д` - да (confirm)
|
||||
/// - `n` / `N` / `т` / `Т` - нет (close)
|
||||
/// - `Enter` - подтвердить (confirm)
|
||||
/// - `Esc` - отменить (close)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key_code` - код нажатой клавиши
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Some(true)` - подтверждение (yes/Enter)
|
||||
/// * `Some(false)` - отмена (no/Escape)
|
||||
/// * `None` - другая клавиша (продолжить ввод)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use crossterm::event::KeyCode;
|
||||
/// use tele_tui::utils::modal_handler::handle_yes_no;
|
||||
///
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('y')), Some(true));
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('Y')), Some(true));
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('н')), Some(true)); // русская 'y'
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Enter), Some(true));
|
||||
///
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('n')), Some(false));
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('т')), Some(false)); // русская 'n'
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Esc), Some(false));
|
||||
///
|
||||
/// assert_eq!(handle_yes_no(KeyCode::Char('a')), None);
|
||||
/// ```
|
||||
pub fn handle_yes_no(key_code: KeyCode) -> Option<bool> {
|
||||
match key_code {
|
||||
// Yes - подтверждение (английская и русская раскладка)
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Char('н') | KeyCode::Char('Н') => {
|
||||
Some(true)
|
||||
}
|
||||
KeyCode::Enter => Some(true),
|
||||
|
||||
// No - отмена (английская и русская раскладка)
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Char('т') | KeyCode::Char('Т') => {
|
||||
Some(false)
|
||||
}
|
||||
KeyCode::Esc => Some(false),
|
||||
|
||||
// Другие клавиши - продолжить
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_handle_yes_no() {
|
||||
// Yes variants
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('y')), Some(true));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('Y')), Some(true));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('н')), Some(true)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('Н')), Some(true)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Enter), Some(true));
|
||||
|
||||
// No variants
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('n')), Some(false));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('N')), Some(false));
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('т')), Some(false)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('Т')), Some(false)); // Russian
|
||||
assert_eq!(handle_yes_no(KeyCode::Esc), Some(false));
|
||||
|
||||
// Other keys
|
||||
assert_eq!(handle_yes_no(KeyCode::Char('a')), None);
|
||||
assert_eq!(handle_yes_no(KeyCode::Up), None);
|
||||
assert_eq!(handle_yes_no(KeyCode::Char(' ')), None);
|
||||
}
|
||||
}
|
||||
167
crates/tele-tui/src/utils/retry.rs
Normal file
167
crates/tele-tui/src/utils/retry.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// Выполняет операцию с таймаутом и возвращает результат.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - Длительность таймаута
|
||||
/// * `operation` - Асинхронная операция для выполнения
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(T)` - если операция успешна
|
||||
/// * `Err(String)` - если операция вернула ошибку или произошел таймаут
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let result = with_timeout(
|
||||
/// Duration::from_secs(5),
|
||||
/// client.load_chats(50)
|
||||
/// ).await;
|
||||
/// ```
|
||||
pub async fn with_timeout<F, T>(duration: Duration, operation: F) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
match timeout(duration, operation).await {
|
||||
Ok(Ok(value)) => Ok(value),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err("Операция превысила время ожидания".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Выполняет операцию с таймаутом и кастомным сообщением об ошибке таймаута.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - Длительность таймаута
|
||||
/// * `operation` - Асинхронная операция для выполнения
|
||||
/// * `timeout_msg` - Сообщение об ошибке при таймауте
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(T)` - если операция успешна
|
||||
/// * `Err(String)` - если операция вернула ошибку или произошел таймаут
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let result = with_timeout_msg(
|
||||
/// Duration::from_secs(5),
|
||||
/// client.load_chats(50),
|
||||
/// "Таймаут загрузки чатов"
|
||||
/// ).await;
|
||||
/// ```
|
||||
pub async fn with_timeout_msg<F, T>(
|
||||
duration: Duration,
|
||||
operation: F,
|
||||
timeout_msg: &str,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
match timeout(duration, operation).await {
|
||||
Ok(Ok(value)) => Ok(value),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(timeout_msg.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Выполняет операцию с таймаутом, игнорируя результат и ошибки.
|
||||
///
|
||||
/// Используется для не критичных операций (например, загрузка дополнительных данных),
|
||||
/// где таймаут или ошибка не должны прерывать основной flow.
|
||||
///
|
||||
/// Работает как с Result<T, E>, так и с void операциями.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - Длительность таймаута
|
||||
/// * `operation` - Асинхронная операция для выполнения
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Загружаем reply info, но не ждём если долго
|
||||
/// with_timeout_ignore(
|
||||
/// Duration::from_secs(5),
|
||||
/// client.fetch_missing_reply_info()
|
||||
/// ).await;
|
||||
/// ```
|
||||
pub async fn with_timeout_ignore<F>(duration: Duration, operation: F)
|
||||
where
|
||||
F: Future,
|
||||
{
|
||||
let _ = timeout(duration, operation).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_success() {
|
||||
let result =
|
||||
with_timeout(Duration::from_secs(1), async { Ok::<_, String>("success".to_string()) })
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "success");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_operation_error() {
|
||||
let result = with_timeout(Duration::from_secs(1), async {
|
||||
Err::<String, _>("operation failed".to_string())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "operation failed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_timeout_error() {
|
||||
let result = with_timeout(Duration::from_millis(10), async {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
Ok::<_, String>("too slow".to_string())
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("превысила время ожидания"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_msg_success() {
|
||||
let result = with_timeout_msg(
|
||||
Duration::from_secs(1),
|
||||
async { Ok::<_, String>("success".to_string()) },
|
||||
"Custom timeout",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "success");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_with_timeout_msg_timeout_error() {
|
||||
let result = with_timeout_msg(
|
||||
Duration::from_millis(10),
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
Ok::<_, String>("too slow".to_string())
|
||||
},
|
||||
"Таймаут загрузки",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "Таймаут загрузки");
|
||||
}
|
||||
}
|
||||
25
crates/tele-tui/src/utils/tdlib.rs
Normal file
25
crates/tele-tui/src/utils/tdlib.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
#[link(name = "tdjson")]
|
||||
extern "C" {
|
||||
fn td_execute(request: *const c_char) -> *const c_char;
|
||||
}
|
||||
|
||||
/// Отключаем логи TDLib синхронно, до создания клиента
|
||||
pub fn disable_tdlib_logs() {
|
||||
let request = r#"{"@type":"setLogVerbosityLevel","new_verbosity_level":0}"#;
|
||||
if let Ok(c_request) = CString::new(request) {
|
||||
unsafe {
|
||||
let _ = td_execute(c_request.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
// Также перенаправляем логи в никуда
|
||||
let request2 = r#"{"@type":"setLogStream","log_stream":{"@type":"logStreamEmpty"}}"#;
|
||||
if let Ok(c_request2) = CString::new(request2) {
|
||||
unsafe {
|
||||
let _ = td_execute(c_request2.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
33
crates/tele-tui/src/utils/validation.rs
Normal file
33
crates/tele-tui/src/utils/validation.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Input validation utilities
|
||||
//!
|
||||
//! Переиспользуемые валидаторы для проверки пользовательского ввода.
|
||||
|
||||
/// Проверяет, что строка не пустая (после trim).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tele_tui::utils::validation::is_non_empty;
|
||||
///
|
||||
/// assert!(is_non_empty("hello"));
|
||||
/// assert!(is_non_empty(" text "));
|
||||
/// assert!(!is_non_empty(""));
|
||||
/// assert!(!is_non_empty(" "));
|
||||
/// ```
|
||||
pub fn is_non_empty(text: &str) -> bool {
|
||||
!text.trim().is_empty()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_non_empty() {
|
||||
assert!(is_non_empty("hello"));
|
||||
assert!(is_non_empty(" text "));
|
||||
assert!(!is_non_empty(""));
|
||||
assert!(!is_non_empty(" "));
|
||||
assert!(!is_non_empty("\t\n"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user