fixes
Some checks failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
Some checks failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
This commit is contained in:
@@ -7,6 +7,7 @@ use crate::tdlib::client::ChatInfo;
|
||||
use crate::tdlib::TdClient;
|
||||
|
||||
pub struct App {
|
||||
pub config: crate::config::Config,
|
||||
pub screen: AppScreen,
|
||||
pub td_client: TdClient,
|
||||
// Auth state
|
||||
@@ -88,11 +89,12 @@ pub struct App {
|
||||
|
||||
|
||||
impl App {
|
||||
pub fn new() -> App {
|
||||
pub fn new(config: crate::config::Config) -> App {
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
|
||||
App {
|
||||
config,
|
||||
screen: AppScreen::Loading,
|
||||
td_client: TdClient::new(),
|
||||
phone_input: String::new(),
|
||||
|
||||
265
src/config.rs
Normal file
265
src/config.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub general: GeneralConfig,
|
||||
#[serde(default)]
|
||||
pub colors: ColorsConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneralConfig {
|
||||
/// Часовой пояс в формате "+03:00" или "-05:00"
|
||||
#[serde(default = "default_timezone")]
|
||||
pub timezone: String,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
// Дефолтные значения
|
||||
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()
|
||||
}
|
||||
|
||||
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 Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
general: GeneralConfig::default(),
|
||||
colors: ColorsConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Путь к конфигурационному файлу
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
/// Загрузить конфигурацию из файла
|
||||
pub fn load() -> Self {
|
||||
let config_path = match Self::config_path() {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
eprintln!("Warning: 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() {
|
||||
eprintln!("Warning: 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) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Could not parse config file: {}", e);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Could not read config file: {}", e);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Сохранить конфигурацию в файл
|
||||
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
|
||||
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 из credentials файла или .env
|
||||
/// Возвращает (api_id, api_hash) или ошибку с инструкциями
|
||||
pub fn load_credentials() -> Result<(i32, String), String> {
|
||||
use std::env;
|
||||
|
||||
// 1. Пробуем загрузить из ~/.config/tele-tui/credentials
|
||||
if let Some(cred_path) = Self::credentials_path() {
|
||||
if cred_path.exists() {
|
||||
if let Ok(content) = fs::read_to_string(&cred_path) {
|
||||
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;
|
||||
}
|
||||
|
||||
if let Some((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());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(id), Some(hash)) = (api_id, api_hash) {
|
||||
return Ok((id, hash));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Пробуем загрузить из переменных окружения (.env)
|
||||
if let (Ok(api_id_str), Ok(api_hash)) = (env::var("API_ID"), env::var("API_HASH")) {
|
||||
if let Ok(api_id) = api_id_str.parse::<i32>() {
|
||||
return Ok((api_id, api_hash));
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod input;
|
||||
mod tdlib;
|
||||
mod ui;
|
||||
@@ -26,6 +27,9 @@ async fn main() -> Result<(), io::Error> {
|
||||
// Загружаем переменные окружения из .env
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
// Загружаем конфигурацию (создаёт дефолтный если отсутствует)
|
||||
let config = config::Config::load();
|
||||
|
||||
// Отключаем логи TDLib ДО создания клиента
|
||||
disable_tdlib_logs();
|
||||
|
||||
@@ -37,7 +41,7 @@ async fn main() -> Result<(), io::Error> {
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Create app state
|
||||
let mut app = App::new();
|
||||
let mut app = App::new(config);
|
||||
let res = run_app(&mut terminal, &mut app).await;
|
||||
|
||||
// Restore terminal
|
||||
|
||||
@@ -264,11 +264,16 @@ pub struct TdClient {
|
||||
#[allow(dead_code)]
|
||||
impl TdClient {
|
||||
pub fn new() -> Self {
|
||||
let api_id: i32 = env::var("API_ID")
|
||||
.unwrap_or_else(|_| "0".to_string())
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
let api_hash = env::var("API_HASH").unwrap_or_default();
|
||||
// Загружаем credentials из ~/.config/tele-tui/credentials или .env
|
||||
let (api_id, api_hash) = match crate::config::Config::load_credentials() {
|
||||
Ok(creds) => creds,
|
||||
Err(err_msg) => {
|
||||
eprintln!("\n{}\n", err_msg);
|
||||
// Используем дефолтные значения, чтобы приложение запустилось
|
||||
// Пользователь увидит сообщение об ошибке в UI
|
||||
(0, String::new())
|
||||
}
|
||||
};
|
||||
|
||||
let client_id = tdlib_rs::create_client();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::utils::{format_timestamp, format_date, get_day};
|
||||
use crate::utils::{format_timestamp_with_tz, format_date, get_day};
|
||||
use tdlib_rs::enums::TextEntityType;
|
||||
use tdlib_rs::types::TextEntity;
|
||||
|
||||
@@ -510,16 +510,16 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
last_sender = Some(current_sender);
|
||||
}
|
||||
|
||||
// Форматируем время (HH:MM)
|
||||
let time = format_timestamp(msg.date);
|
||||
// Форматируем время (HH:MM) с учётом timezone из config
|
||||
let time = format_timestamp_with_tz(msg.date, &app.config.general.timezone);
|
||||
|
||||
// Цвет сообщения (жёлтый если выбрано)
|
||||
// Цвет сообщения (из config или жёлтый если выбрано)
|
||||
let msg_color = if is_selected {
|
||||
Color::Yellow
|
||||
app.config.parse_color(&app.config.colors.selected_message)
|
||||
} else if msg.is_outgoing {
|
||||
Color::Green
|
||||
app.config.parse_color(&app.config.colors.outgoing_message)
|
||||
} else {
|
||||
Color::White
|
||||
app.config.parse_color(&app.config.colors.incoming_message)
|
||||
};
|
||||
|
||||
// Маркер выбора
|
||||
@@ -694,9 +694,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
|
||||
let style = if reaction.is_chosen {
|
||||
Style::default().fg(Color::Yellow)
|
||||
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_chosen))
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_other))
|
||||
};
|
||||
|
||||
reaction_spans.push(Span::styled(reaction_text, style));
|
||||
|
||||
36
src/utils.rs
36
src/utils.rs
@@ -22,20 +22,42 @@ pub fn disable_tdlib_logs() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в время HH:MM
|
||||
pub fn format_timestamp(timestamp: i32) -> String {
|
||||
/// Форматирование timestamp в время HH:MM с учётом timezone offset
|
||||
/// timezone_str: строка формата "+03:00" или "-05:00"
|
||||
pub fn format_timestamp_with_tz(timestamp: i32, timezone_str: &str) -> String {
|
||||
let secs = timestamp as i64;
|
||||
// Конвертируем в локальное время (простой способ без chrono)
|
||||
// UTC + смещение для локального времени
|
||||
let hours = ((secs % 86400) / 3600) as u32;
|
||||
|
||||
// Парсим timezone offset (например "+03:00" -> 3 часа)
|
||||
let offset_hours = parse_timezone_offset(timezone_str);
|
||||
|
||||
let hours = ((secs % 86400) / 3600) as i32;
|
||||
let minutes = ((secs % 3600) / 60) as u32;
|
||||
|
||||
// Примерное локальное время (добавим 3 часа для MSK, можно настроить)
|
||||
let local_hours = (hours + 3) % 24;
|
||||
// Применяем timezone offset
|
||||
let local_hours = ((hours + offset_hours) % 24 + 24) % 24;
|
||||
|
||||
format!("{:02}:{:02}", local_hours, minutes)
|
||||
}
|
||||
|
||||
/// Парсит timezone строку типа "+03:00" в количество часов
|
||||
fn parse_timezone_offset(tz: &str) -> i32 {
|
||||
// Простой парсинг "+03:00" или "-05:00"
|
||||
if tz.len() >= 3 {
|
||||
let sign = if tz.starts_with('-') { -1 } else { 1 };
|
||||
let hours_str = &tz[1..3];
|
||||
if let Ok(hours) = hours_str.parse::<i32>() {
|
||||
return sign * hours;
|
||||
}
|
||||
}
|
||||
3 // fallback к MSK
|
||||
}
|
||||
|
||||
/// Устаревшая функция для обратной совместимости (используется дефолтный +03:00)
|
||||
#[allow(dead_code)]
|
||||
pub fn format_timestamp(timestamp: i32) -> String {
|
||||
format_timestamp_with_tz(timestamp, "+03:00")
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в дату для разделителя
|
||||
pub fn format_date(timestamp: i32) -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
Reference in New Issue
Block a user