style: auto-format entire codebase with cargo fmt (stable rustfmt.toml)
Some checks failed
ci/woodpecker/pr/check Pipeline 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:
Mikhail Kilin
2026-02-22 17:09:51 +03:00
parent 2442a90e23
commit 264f183510
90 changed files with 1632 additions and 1450 deletions

View File

@@ -1,6 +1,6 @@
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crate::tdlib::AuthState;
use crate::tdlib::TdClientTrait;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},

View File

@@ -1,7 +1,7 @@
//! Chat list panel: search box, chat items, and user online status.
use crate::app::App;
use crate::app::methods::{compose::ComposeMethods, search::SearchMethods};
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crate::tdlib::UserOnlineStatus;
use crate::ui::components;
@@ -76,7 +76,9 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
app.selected_chat_id
} else {
let filtered = app.get_filtered_chats();
app.chat_list_state.selected().and_then(|i| filtered.get(i).map(|c| c.id))
app.chat_list_state
.selected()
.and_then(|i| filtered.get(i).map(|c| c.id))
};
let (status_text, status_color) = match status_chat_id {
Some(chat_id) => format_user_status(app.td_client.get_user_status_by_chat_id(chat_id)),

View File

@@ -29,12 +29,7 @@ pub fn render_emoji_picker(
let x = area.x + (area.width.saturating_sub(modal_width)) / 2;
let y = area.y + (area.height.saturating_sub(modal_height)) / 2;
let modal_area = Rect::new(
x,
y,
modal_width.min(area.width),
modal_height.min(area.height),
);
let modal_area = Rect::new(x, y, modal_width.min(area.width), modal_height.min(area.height));
// Очищаем область под модалкой
f.render_widget(Clear, modal_area);
@@ -87,10 +82,7 @@ pub fn render_emoji_picker(
.add_modifier(Modifier::BOLD),
),
Span::raw("Добавить "),
Span::styled(
" [Esc] ",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw("Отмена"),
]));

View File

@@ -34,10 +34,7 @@ pub fn render_input_field(
// Символ под курсором (или █ если курсор в конце)
if safe_cursor_pos < chars.len() {
let cursor_char = chars[safe_cursor_pos].to_string();
spans.push(Span::styled(
cursor_char,
Style::default().fg(Color::Black).bg(color),
));
spans.push(Span::styled(cursor_char, Style::default().fg(Color::Black).bg(color)));
} else {
// Курсор в конце - показываем блок
spans.push(Span::styled("", Style::default().fg(color)));

View File

@@ -7,9 +7,9 @@
use crate::config::Config;
use crate::formatting;
use crate::tdlib::{MessageInfo, PlaybackState, PlaybackStatus};
#[cfg(feature = "images")]
use crate::tdlib::PhotoDownloadState;
use crate::tdlib::{MessageInfo, PlaybackState, PlaybackStatus};
use crate::types::MessageId;
use crate::utils::{format_date, format_timestamp_with_tz};
use ratatui::{
@@ -36,10 +36,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
}
if all_lines.is_empty() {
all_lines.push(WrappedLine {
text: String::new(),
start_offset: 0,
});
all_lines.push(WrappedLine { text: String::new(), start_offset: 0 });
}
all_lines
@@ -48,10 +45,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
/// Разбивает один абзац (без `\n`) на строки по ширине
fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec<WrappedLine> {
if max_width == 0 {
return vec![WrappedLine {
text: text.to_string(),
start_offset: base_offset,
}];
return vec![WrappedLine { text: text.to_string(), start_offset: base_offset }];
}
let mut result = Vec::new();
@@ -122,10 +116,7 @@ fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec<Wrapp
}
if result.is_empty() {
result.push(WrappedLine {
text: String::new(),
start_offset: base_offset,
});
result.push(WrappedLine { text: String::new(), start_offset: base_offset });
}
result
@@ -138,7 +129,11 @@ fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec<Wrapp
/// * `date` - timestamp сообщения
/// * `content_width` - ширина области для центрирования
/// * `is_first` - первый ли это разделитель (если нет, добавляется пустая строка сверху)
pub fn render_date_separator(date: i32, content_width: usize, is_first: bool) -> Vec<Line<'static>> {
pub fn render_date_separator(
date: i32,
content_width: usize,
is_first: bool,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
if !is_first {
@@ -276,10 +271,8 @@ pub fn render_message_bubble(
Span::styled(reply_line, Style::default().fg(Color::Cyan)),
]));
} else {
lines.push(Line::from(vec![Span::styled(
reply_line,
Style::default().fg(Color::Cyan),
)]));
lines
.push(Line::from(vec![Span::styled(reply_line, Style::default().fg(Color::Cyan))]));
}
}
@@ -301,9 +294,13 @@ pub fn render_message_bubble(
let is_last_line = i == total_wrapped - 1;
let line_len = wrapped.text.chars().count();
let line_entities =
formatting::adjust_entities_for_substring(msg.entities(), wrapped.start_offset, line_len);
let formatted_spans = formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
let line_entities = formatting::adjust_entities_for_substring(
msg.entities(),
wrapped.start_offset,
line_len,
);
let formatted_spans =
formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
if is_last_line {
let full_len = line_len + time_mark_len + marker_len;
@@ -313,14 +310,19 @@ pub fn render_message_bubble(
// Одна строка — маркер на ней
line_spans.push(Span::styled(
selection_marker,
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} else if is_selected {
// Последняя строка multi-line — пробелы вместо маркера
line_spans.push(Span::raw(" ".repeat(marker_len)));
}
line_spans.extend(formatted_spans);
line_spans.push(Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)));
line_spans.push(Span::styled(
format!(" {}", time_mark),
Style::default().fg(Color::Gray),
));
lines.push(Line::from(line_spans));
} else {
let padding = content_width.saturating_sub(line_len + marker_len + 1);
@@ -328,7 +330,9 @@ pub fn render_message_bubble(
if i == 0 && is_selected {
line_spans.push(Span::styled(
selection_marker,
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} else if is_selected {
// Средние строки multi-line — пробелы вместо маркера
@@ -350,19 +354,26 @@ pub fn render_message_bubble(
for (i, wrapped) in wrapped_lines.into_iter().enumerate() {
let line_len = wrapped.text.chars().count();
let line_entities =
formatting::adjust_entities_for_substring(msg.entities(), wrapped.start_offset, line_len);
let formatted_spans = formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
let line_entities = formatting::adjust_entities_for_substring(
msg.entities(),
wrapped.start_offset,
line_len,
);
let formatted_spans =
formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
if i == 0 {
let mut line_spans = vec![];
if is_selected {
line_spans.push(Span::styled(
selection_marker,
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
}
line_spans.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)));
line_spans
.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)));
line_spans.push(Span::raw(" "));
line_spans.extend(formatted_spans);
lines.push(Line::from(line_spans));
@@ -439,10 +450,7 @@ pub fn render_message_bubble(
_ => "",
};
let bar = render_progress_bar(ps.position, ps.duration, 20);
format!(
"{} {} {:.0}s/{:.0}s",
icon, bar, ps.position, ps.duration
)
format!("{} {} {:.0}s/{:.0}s", icon, bar, ps.position, ps.duration)
} else {
let waveform = render_waveform(&voice.waveform, 20);
format!(" {} {:.0}s", waveform, voice.duration)
@@ -456,10 +464,7 @@ pub fn render_message_bubble(
Span::styled(status_line, Style::default().fg(Color::Cyan)),
]));
} else {
lines.push(Line::from(Span::styled(
status_line,
Style::default().fg(Color::Cyan),
)));
lines.push(Line::from(Span::styled(status_line, Style::default().fg(Color::Cyan))));
}
}
}
@@ -477,10 +482,8 @@ pub fn render_message_bubble(
Span::styled(status, Style::default().fg(Color::Yellow)),
]));
} else {
lines.push(Line::from(Span::styled(
status,
Style::default().fg(Color::Yellow),
)));
lines
.push(Line::from(Span::styled(status, Style::default().fg(Color::Yellow))));
}
}
PhotoDownloadState::Error(e) => {
@@ -492,10 +495,7 @@ pub fn render_message_bubble(
Span::styled(status, Style::default().fg(Color::Red)),
]));
} else {
lines.push(Line::from(Span::styled(
status,
Style::default().fg(Color::Red),
)));
lines.push(Line::from(Span::styled(status, Style::default().fg(Color::Red))));
}
}
PhotoDownloadState::Downloaded(_) => {
@@ -540,7 +540,9 @@ pub fn render_album_bubble(
content_width: usize,
selected_msg_id: Option<MessageId>,
) -> (Vec<Line<'static>>, Vec<DeferredImageRender>) {
use crate::constants::{ALBUM_GRID_MAX_COLS, ALBUM_PHOTO_GAP, ALBUM_PHOTO_HEIGHT, ALBUM_PHOTO_WIDTH};
use crate::constants::{
ALBUM_GRID_MAX_COLS, ALBUM_PHOTO_GAP, ALBUM_PHOTO_HEIGHT, ALBUM_PHOTO_WIDTH,
};
let mut lines: Vec<Line<'static>> = Vec::new();
let mut deferred: Vec<DeferredImageRender> = Vec::new();
@@ -569,12 +571,12 @@ pub fn render_album_bubble(
// Добавляем маркер выбора на первую строку
if is_selected {
lines.push(Line::from(vec![
Span::styled(
selection_marker,
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]));
}
let grid_start_line = lines.len();
@@ -608,7 +610,9 @@ pub fn render_album_bubble(
let x_off = if is_outgoing {
let grid_width = cols as u16 * ALBUM_PHOTO_WIDTH
+ (cols as u16).saturating_sub(1) * ALBUM_PHOTO_GAP;
let padding = content_width.saturating_sub(grid_width as usize + 1) as u16;
let padding = content_width
.saturating_sub(grid_width as usize + 1)
as u16;
padding + col as u16 * (ALBUM_PHOTO_WIDTH + ALBUM_PHOTO_GAP)
} else {
col as u16 * (ALBUM_PHOTO_WIDTH + ALBUM_PHOTO_GAP)
@@ -617,7 +621,8 @@ pub fn render_album_bubble(
deferred.push(DeferredImageRender {
message_id: msg.id(),
photo_path: path.clone(),
line_offset: grid_start_line + row * ALBUM_PHOTO_HEIGHT as usize,
line_offset: grid_start_line
+ row * ALBUM_PHOTO_HEIGHT as usize,
x_offset: x_off,
width: ALBUM_PHOTO_WIDTH,
height: ALBUM_PHOTO_HEIGHT,
@@ -644,10 +649,7 @@ pub fn render_album_bubble(
}
PhotoDownloadState::NotDownloaded => {
if line_in_row == ALBUM_PHOTO_HEIGHT / 2 {
spans.push(Span::styled(
"📷",
Style::default().fg(Color::Gray),
));
spans.push(Span::styled("📷", Style::default().fg(Color::Gray)));
}
}
}
@@ -706,9 +708,10 @@ pub fn render_album_bubble(
Span::styled(time_text, Style::default().fg(Color::Gray)),
]));
} else {
lines.push(Line::from(vec![
Span::styled(format!(" {}", time_text), Style::default().fg(Color::Gray)),
]));
lines.push(Line::from(vec![Span::styled(
format!(" {}", time_text),
Style::default().fg(Color::Gray),
)]));
}
}

View File

@@ -91,7 +91,10 @@ pub fn calculate_scroll_offset(
}
/// Renders a help bar with keyboard shortcuts
pub fn render_help_bar(shortcuts: &[(&str, &str, Color)], border_color: Color) -> Paragraph<'static> {
pub fn render_help_bar(
shortcuts: &[(&str, &str, Color)],
border_color: Color,
) -> Paragraph<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
for (i, (key, label, color)) in shortcuts.iter().enumerate() {
if i > 0 {
@@ -99,9 +102,7 @@ pub fn render_help_bar(shortcuts: &[(&str, &str, Color)], border_color: Color) -
}
spans.push(Span::styled(
format!(" {} ", key),
Style::default()
.fg(*color)
.add_modifier(Modifier::BOLD),
Style::default().fg(*color).add_modifier(Modifier::BOLD),
));
spans.push(Span::raw(label.to_string()));
}

View File

@@ -1,17 +1,17 @@
//! Reusable UI components: message bubbles, input fields, modals, lists.
pub mod modal;
pub mod chat_list_item;
pub mod emoji_picker;
pub mod input_field;
pub mod message_bubble;
pub mod message_list;
pub mod chat_list_item;
pub mod emoji_picker;
pub mod modal;
// Экспорт основных функций
pub use input_field::render_input_field;
pub use chat_list_item::render_chat_list_item;
pub use emoji_picker::render_emoji_picker;
pub use message_bubble::{render_date_separator, render_message_bubble, render_sender_header};
pub use input_field::render_input_field;
#[cfg(feature = "images")]
pub use message_bubble::{DeferredImageRender, calculate_image_height, render_album_bubble};
pub use message_list::{render_message_item, calculate_scroll_offset, render_help_bar};
pub use message_bubble::{calculate_image_height, render_album_bubble, DeferredImageRender};
pub use message_bubble::{render_date_separator, render_message_bubble, render_sender_header};
pub use message_list::{calculate_scroll_offset, render_help_bar, render_message_item};

View File

@@ -74,10 +74,7 @@ pub fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
),
Span::raw("Да"),
Span::raw(" "),
Span::styled(
" [n/Esc] ",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
Span::raw("Нет"),
]),
];

View File

@@ -1,8 +1,8 @@
//! Compose bar / input box rendering
use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods};
use crate::app::App;
use crate::app::InputMode;
use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods};
use crate::tdlib::TdClientTrait;
use crate::ui::components;
use ratatui::{
@@ -124,13 +124,18 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
} else if app.input_mode == InputMode::Normal {
// Normal mode — dim, no cursor
if app.message_input.is_empty() {
let line = Line::from(vec![
Span::styled("> Press i to type...", Style::default().fg(Color::DarkGray)),
]);
let line = Line::from(vec![Span::styled(
"> Press i to type...",
Style::default().fg(Color::DarkGray),
)]);
(line, "")
} else {
let draft_preview: String = app.message_input.chars().take(60).collect();
let ellipsis = if app.message_input.chars().count() > 60 { "..." } else { "" };
let ellipsis = if app.message_input.chars().count() > 60 {
"..."
} else {
""
};
let line = Line::from(Span::styled(
format!("> {}{}", draft_preview, ellipsis),
Style::default().fg(Color::DarkGray),
@@ -163,7 +168,9 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
} else {
Style::default().fg(Color::DarkGray)
};
Block::default().borders(Borders::ALL).border_style(border_style)
Block::default()
.borders(Borders::ALL)
.border_style(border_style)
} else {
let title_color = if app.is_replying() || app.is_forwarding() {
Color::Cyan

View File

@@ -1,7 +1,7 @@
use crate::app::App;
use crate::app::InputMode;
use crate::tdlib::TdClientTrait;
use crate::tdlib::NetworkState;
use crate::tdlib::TdClientTrait;
use ratatui::{
layout::Rect,
style::{Color, Style},
@@ -31,7 +31,10 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
} else if let Some(err) = &app.error_message {
format!(" {}{}Error: {} ", account_indicator, network_indicator, err)
} else if app.is_searching {
format!(" {}{}↑/↓: Navigate | Enter: Select | Esc: Cancel ", account_indicator, network_indicator)
format!(
" {}{}↑/↓: Navigate | Enter: Select | Esc: Cancel ",
account_indicator, network_indicator
)
} else if app.selected_chat_id.is_some() {
let mode_str = match app.input_mode {
InputMode::Normal => "[NORMAL] j/k: Nav | i: Insert | d/r/f/y: Actions | Esc: Close",

View File

@@ -3,10 +3,10 @@
//! Renders message bubbles grouped by date/sender, pinned bar, and delegates
//! to modals (search, pinned, reactions, delete) and compose_bar.
use crate::app::App;
use crate::app::methods::{messages::MessageMethods, modal::ModalMethods, search::SearchMethods};
use crate::tdlib::TdClientTrait;
use crate::app::App;
use crate::message_grouping::{group_messages, MessageGroup};
use crate::tdlib::TdClientTrait;
use crate::ui::components;
use crate::ui::{compose_bar, modals};
use ratatui::{
@@ -18,7 +18,12 @@ use ratatui::{
};
/// Рендерит заголовок чата с typing status
fn render_chat_header<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>, chat: &crate::tdlib::ChatInfo) {
fn render_chat_header<T: TdClientTrait>(
f: &mut Frame,
area: Rect,
app: &App<T>,
chat: &crate::tdlib::ChatInfo,
) {
let typing_action = app
.td_client
.typing_status()
@@ -34,10 +39,7 @@ fn render_chat_header<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>,
.add_modifier(Modifier::BOLD),
)];
if let Some(username) = &chat.username {
spans.push(Span::styled(
format!(" {}", username),
Style::default().fg(Color::Gray),
));
spans.push(Span::styled(format!(" {}", username), Style::default().fg(Color::Gray)));
}
spans.push(Span::styled(
format!(" {}", action),
@@ -90,8 +92,7 @@ fn render_pinned_bar<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>)
Span::raw(" ".repeat(padding)),
Span::styled(pinned_hint, Style::default().fg(Color::Gray)),
]);
let pinned_bar =
Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40)));
let pinned_bar = Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40)));
f.render_widget(pinned_bar, area);
}
@@ -104,9 +105,7 @@ pub(super) struct WrappedLine {
/// (используется только для search/pinned режимов, основной рендеринг через message_bubble)
pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
if max_width == 0 {
return vec![WrappedLine {
text: text.to_string(),
}];
return vec![WrappedLine { text: text.to_string() }];
}
let mut result = Vec::new();
@@ -131,9 +130,7 @@ pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<Wrappe
current_line.push_str(&word);
current_width += 1 + word_width;
} else {
result.push(WrappedLine {
text: current_line,
});
result.push(WrappedLine { text: current_line });
current_line = word;
current_width = word_width;
}
@@ -155,23 +152,17 @@ pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<Wrappe
current_line.push(' ');
current_line.push_str(&word);
} else {
result.push(WrappedLine {
text: current_line,
});
result.push(WrappedLine { text: current_line });
current_line = word;
}
}
if !current_line.is_empty() {
result.push(WrappedLine {
text: current_line,
});
result.push(WrappedLine { text: current_line });
}
if result.is_empty() {
result.push(WrappedLine {
text: String::new(),
});
result.push(WrappedLine { text: String::new() });
}
result
@@ -208,10 +199,7 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
is_first_date = false;
is_first_sender = true; // Сбрасываем счётчик заголовков после даты
}
MessageGroup::SenderHeader {
is_outgoing,
sender_name,
} => {
MessageGroup::SenderHeader { is_outgoing, sender_name } => {
// Рендерим заголовок отправителя
lines.extend(components::render_sender_header(
is_outgoing,
@@ -240,9 +228,16 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
// Собираем deferred image renders для всех загруженных фото
#[cfg(feature = "images")]
if let Some(photo) = msg.photo_info() {
if let crate::tdlib::PhotoDownloadState::Downloaded(path) = &photo.download_state {
let inline_width = content_width.min(crate::constants::INLINE_IMAGE_MAX_WIDTH);
let img_height = components::calculate_image_height(photo.width, photo.height, inline_width);
if let crate::tdlib::PhotoDownloadState::Downloaded(path) =
&photo.download_state
{
let inline_width =
content_width.min(crate::constants::INLINE_IMAGE_MAX_WIDTH);
let img_height = components::calculate_image_height(
photo.width,
photo.height,
inline_width,
);
let img_width = inline_width as u16;
let bubble_len = bubble_lines.len();
let placeholder_start = lines.len() + bubble_len - img_height as usize;
@@ -352,7 +347,8 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
use ratatui_image::StatefulImage;
// THROTTLING: Рендерим изображения максимум 15 FPS (каждые 66ms)
let should_render_images = app.last_image_render_time
let should_render_images = app
.last_image_render_time
.map(|t| t.elapsed() > std::time::Duration::from_millis(66))
.unwrap_or(true);
@@ -384,7 +380,7 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
if let Some(renderer) = &mut app.inline_image_renderer {
// Загружаем только если видимо (early return если уже в кеше)
let _ = renderer.load_image(d.message_id, &d.photo_path);
if let Some(protocol) = renderer.get_protocol(&d.message_id) {
f.render_stateful_widget(StatefulImage::default(), img_rect, protocol);
}
@@ -487,14 +483,9 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
}
// Модалка выбора реакции
if let crate::app::ChatState::ReactionPicker {
available_reactions,
selected_index,
..
} = &app.chat_state
if let crate::app::ChatState::ReactionPicker { available_reactions, selected_index, .. } =
&app.chat_state
{
modals::render_reaction_picker(f, area, available_reactions, *selected_index);
}
}

View File

@@ -4,8 +4,8 @@
mod auth;
pub mod chat_list;
mod compose_bar;
pub mod components;
mod compose_bar;
pub mod footer;
mod loading;
mod main_screen;

View File

@@ -20,18 +20,10 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
};
match state {
AccountSwitcherState::SelectAccount {
accounts,
selected_index,
current_account,
} => {
AccountSwitcherState::SelectAccount { accounts, selected_index, current_account } => {
render_select_account(f, area, accounts, *selected_index, current_account);
}
AccountSwitcherState::AddAccount {
name_input,
cursor_position,
error,
} => {
AccountSwitcherState::AddAccount { name_input, cursor_position, error } => {
render_add_account(f, area, name_input, *cursor_position, error.as_deref());
}
}
@@ -53,10 +45,7 @@ fn render_select_account(
let marker = if is_current { "" } else { " " };
let suffix = if is_current { " (текущий)" } else { "" };
let display = format!(
"{}{} ({}){}",
marker, account.name, account.display_name, suffix
);
let display = format!("{}{} ({}){}", marker, account.name, account.display_name, suffix);
let style = if is_selected {
Style::default()
@@ -86,10 +75,7 @@ fn render_select_account(
} else {
Style::default().fg(Color::Cyan)
};
lines.push(Line::from(Span::styled(
" + Добавить аккаунт",
add_style,
)));
lines.push(Line::from(Span::styled(" + Добавить аккаунт", add_style)));
lines.push(Line::from(""));
@@ -148,10 +134,7 @@ fn render_add_account(
let input_display = if name_input.is_empty() {
Span::styled("_", Style::default().fg(Color::DarkGray))
} else {
Span::styled(
format!("{}_", name_input),
Style::default().fg(Color::White),
)
Span::styled(format!("{}_", name_input), Style::default().fg(Color::White))
};
lines.push(Line::from(vec![
Span::styled(" Имя: ", Style::default().fg(Color::Cyan)),
@@ -168,10 +151,7 @@ fn render_add_account(
// Error
if let Some(err) = error {
lines.push(Line::from(Span::styled(
format!(" {}", err),
Style::default().fg(Color::Red),
)));
lines.push(Line::from(Span::styled(format!(" {}", err), Style::default().fg(Color::Red))));
lines.push(Line::from(""));
}

View File

@@ -1,6 +1,6 @@
//! Delete confirmation modal
use ratatui::{Frame, layout::Rect};
use ratatui::{layout::Rect, Frame};
/// Renders delete confirmation modal
pub fn render(f: &mut Frame, area: Rect) {

View File

@@ -19,19 +19,12 @@ use ratatui::{
use ratatui_image::StatefulImage;
/// Рендерит модальное окно с полноэкранным изображением
pub fn render<T: TdClientTrait>(
f: &mut Frame,
app: &mut App<T>,
modal_state: &ImageModalState,
) {
pub fn render<T: TdClientTrait>(f: &mut Frame, app: &mut App<T>, modal_state: &ImageModalState) {
let area = f.area();
// Затемняем весь фон
f.render_widget(Clear, area);
f.render_widget(
Block::default().style(Style::default().bg(Color::Black)),
area,
);
f.render_widget(Block::default().style(Style::default().bg(Color::Black)), area);
// Резервируем место для подсказок (2 строки внизу)
let image_area_height = area.height.saturating_sub(2);
@@ -76,7 +69,7 @@ pub fn render<T: TdClientTrait>(
// Загружаем изображение (может занять время для iTerm2/Sixel)
let _ = renderer.load_image(modal_state.message_id, &modal_state.photo_path);
// Триггерим перерисовку для показа загруженного изображения
app.needs_redraw = true;
}

View File

@@ -10,18 +10,18 @@
pub mod account_switcher;
pub mod delete_confirm;
pub mod pinned;
pub mod reaction_picker;
pub mod search;
pub mod pinned;
#[cfg(feature = "images")]
pub mod image_viewer;
pub use account_switcher::render as render_account_switcher;
pub use delete_confirm::render as render_delete_confirm;
pub use pinned::render as render_pinned;
pub use reaction_picker::render as render_reaction_picker;
pub use search::render as render_search;
pub use pinned::render as render_pinned;
#[cfg(feature = "images")]
pub use image_viewer::render as render_image_viewer;

View File

@@ -2,7 +2,7 @@
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crate::ui::components::{render_message_item, calculate_scroll_offset, render_help_bar};
use crate::ui::components::{calculate_scroll_offset, render_help_bar, render_message_item};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
@@ -14,15 +14,13 @@ use ratatui::{
/// Renders pinned messages mode
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Извлекаем данные из ChatState
let (messages, selected_index) = if let crate::app::ChatState::PinnedMessages {
messages,
selected_index,
} = &app.chat_state
{
(messages.as_slice(), *selected_index)
} else {
return; // Некорректное состояние
};
let (messages, selected_index) =
if let crate::app::ChatState::PinnedMessages { messages, selected_index } = &app.chat_state
{
(messages.as_slice(), *selected_index)
} else {
return; // Некорректное состояние
};
let chunks = Layout::default()
.direction(Direction::Vertical)

View File

@@ -1,13 +1,8 @@
//! Reaction picker modal
use ratatui::{Frame, layout::Rect};
use ratatui::{layout::Rect, Frame};
/// Renders emoji reaction picker modal
pub fn render(
f: &mut Frame,
area: Rect,
available_reactions: &[String],
selected_index: usize,
) {
pub fn render(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) {
crate::ui::components::render_emoji_picker(f, area, available_reactions, selected_index);
}

View File

@@ -2,7 +2,7 @@
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crate::ui::components::{render_message_item, calculate_scroll_offset, render_help_bar};
use crate::ui::components::{calculate_scroll_offset, render_help_bar, render_message_item};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
@@ -15,11 +15,8 @@ use ratatui::{
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Извлекаем данные из ChatState
let (query, results, selected_index) =
if let crate::app::ChatState::SearchInChat {
query,
results,
selected_index,
} = &app.chat_state
if let crate::app::ChatState::SearchInChat { query, results, selected_index } =
&app.chat_state
{
(query.as_str(), results.as_slice(), *selected_index)
} else {
@@ -37,11 +34,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Search input
let total = results.len();
let current = if total > 0 {
selected_index + 1
} else {
0
};
let current = if total > 0 { selected_index + 1 } else { 0 };
let input_line = if query.is_empty() {
Line::from(vec![

View File

@@ -1,7 +1,7 @@
use crate::app::App;
use crate::app::methods::modal::ModalMethods;
use crate::tdlib::TdClientTrait;
use crate::app::App;
use crate::tdlib::ProfileInfo;
use crate::tdlib::TdClientTrait;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},