Compare commits

..

2 Commits

Author SHA1 Message Date
564df43910 Merge pull request 'fix: always reserve space for selection marker to prevent text shift' (#23) from refactor into main
Reviewed-on: #23
2026-02-24 12:59:04 +00:00
Mikhail Kilin
a095fe277b fix: always reserve space for selection marker to prevent text shift
Some checks failed
ci/woodpecker/pr/check Pipeline failed
Render "  " (2 spaces) for unselected messages instead of nothing,
so text stays aligned when navigating with the ▶ selection indicator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:49:08 +03:00
22 changed files with 54 additions and 58 deletions

View File

@@ -221,9 +221,9 @@ pub fn render_message_bubble(
let mut lines = Vec::new(); let mut lines = Vec::new();
let is_selected = selected_msg_id == Some(msg.id()); let is_selected = selected_msg_id == Some(msg.id());
// Маркер выбора // Маркер выбора (всегда резервируем место для ▶, чтобы текст не сдвигался)
let selection_marker = if is_selected { "" } else { "" }; let selection_marker = if is_selected { "" } else { " " };
let marker_len = selection_marker.chars().count(); let marker_len = 2;
// Цвет сообщения // Цвет сообщения
let msg_color = if is_selected { let msg_color = if is_selected {
@@ -306,16 +306,16 @@ pub fn render_message_bubble(
let full_len = line_len + time_mark_len + marker_len; let full_len = line_len + time_mark_len + marker_len;
let padding = content_width.saturating_sub(full_len + 1); let padding = content_width.saturating_sub(full_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))]; let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if is_selected && i == 0 { if i == 0 {
// Одна строка — маркер на ней // Первая (или единственная) строка — маркер
line_spans.push(Span::styled( line_spans.push(Span::styled(
selection_marker, selection_marker,
Style::default() Style::default()
.fg(Color::Yellow) .fg(Color::Yellow)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
)); ));
} else if is_selected { } else {
// Последняя строка multi-line — пробелы вместо маркера // Остальные строки multi-line — пробелы вместо маркера
line_spans.push(Span::raw(" ".repeat(marker_len))); line_spans.push(Span::raw(" ".repeat(marker_len)));
} }
line_spans.extend(formatted_spans); line_spans.extend(formatted_spans);
@@ -327,14 +327,14 @@ pub fn render_message_bubble(
} else { } else {
let padding = content_width.saturating_sub(line_len + marker_len + 1); let padding = content_width.saturating_sub(line_len + marker_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))]; let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if i == 0 && is_selected { if i == 0 {
line_spans.push(Span::styled( line_spans.push(Span::styled(
selection_marker, selection_marker,
Style::default() Style::default()
.fg(Color::Yellow) .fg(Color::Yellow)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
)); ));
} else if is_selected { } else {
// Средние строки multi-line — пробелы вместо маркера // Средние строки multi-line — пробелы вместо маркера
line_spans.push(Span::raw(" ".repeat(marker_len))); line_spans.push(Span::raw(" ".repeat(marker_len)));
} }
@@ -364,14 +364,12 @@ pub fn render_message_bubble(
if i == 0 { if i == 0 {
let mut line_spans = vec![]; let mut line_spans = vec![];
if is_selected { line_spans.push(Span::styled(
line_spans.push(Span::styled( selection_marker,
selection_marker, Style::default()
Style::default() .fg(Color::Yellow)
.fg(Color::Yellow) .add_modifier(Modifier::BOLD),
.add_modifier(Modifier::BOLD), ));
));
}
line_spans line_spans
.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray))); .push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)));
line_spans.push(Span::raw(" ")); line_spans.push(Span::raw(" "));
@@ -548,8 +546,8 @@ pub fn render_album_bubble(
let is_selected = messages.iter().any(|m| selected_msg_id == Some(m.id())); let is_selected = messages.iter().any(|m| selected_msg_id == Some(m.id()));
let is_outgoing = messages.first().is_some_and(|m| m.is_outgoing()); let is_outgoing = messages.first().is_some_and(|m| m.is_outgoing());
// Selection marker // Selection marker (всегда резервируем место)
let selection_marker = if is_selected { "" } else { "" }; let selection_marker = if is_selected { "" } else { " " };
// Фильтруем фото // Фильтруем фото
let photos: Vec<&MessageInfo> = messages.iter().filter(|m| m.has_photo()).collect(); let photos: Vec<&MessageInfo> = messages.iter().filter(|m| m.has_photo()).collect();
@@ -567,15 +565,13 @@ pub fn render_album_bubble(
let cols = photo_count.min(ALBUM_GRID_MAX_COLS); let cols = photo_count.min(ALBUM_GRID_MAX_COLS);
let rows = photo_count.div_ceil(cols); let rows = photo_count.div_ceil(cols);
// Добавляем маркер выбора на первую строку // Добавляем маркер выбора на первую строку (всегда — для постоянного отступа)
if is_selected { lines.push(Line::from(vec![Span::styled(
lines.push(Line::from(vec![Span::styled( selection_marker,
selection_marker, Style::default()
Style::default() .fg(Color::Yellow)
.fg(Color::Yellow) .add_modifier(Modifier::BOLD),
.add_modifier(Modifier::BOLD), )]));
)]));
}
let grid_start_line = lines.len(); let grid_start_line = lines.len();

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│Mom ──────────────── │ │Mom ──────────────── │
│ (14:33) What do you think about this? (14:33) What do you think about this? │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,9 +9,9 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│Alice ──────────────── │ │Alice ──────────────── │
│ (14:33) 📷 [Фото] (14:33) 📷 [Фото] │
│ (14:33) Caption for album (14:33) Caption for album │
│ (14:33) 📷 [Фото] (14:33) 📷 [Фото] │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│Alice ──────────────── │ │Alice ──────────────── │
│ (14:33) 📷 [Фото] (14:33) 📷 [Фото] │
│▶ (14:33) 📷 [Фото] │ │▶ (14:33) 📷 [Фото] │
│ │ │ │
│ │ │ │

View File

@@ -9,10 +9,10 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│Alice ──────────────── │ │Alice ──────────────── │
│ (14:33) Regular message before (14:33) Regular message before │
│ (14:33) 📷 [Фото] (14:33) 📷 [Фото] │
│ (14:33) Album caption (14:33) Album caption │
│ (14:33) Regular message after (14:33) Regular message after │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) Message from the past (14:33) Message from the past │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33 ✎) Edited text (14:33 ✎) Edited text │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -10,7 +10,7 @@ expression: output
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│↪ Переслано от Alice │ │↪ Переслано от Alice │
│ (14:33) Forwarded content (14:33) Forwarded content │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,9 +9,9 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) This is a very long message that should wrap across multiple lines (14:33) This is a very long message that should wrap across multiple lines │
│ when rendered in the terminal UI. Let's make it even longer to when rendered in the terminal UI. Let's make it even longer to │
│ ensure we test the wrapping behavior properly. ensure we test the wrapping behavior properly. │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) **bold** *italic* `code` (14:33) **bold** *italic* `code` │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) Check [this](https://example.com) and @username (14:33) Check [this](https://example.com) and @username │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) Spoiler: ||hidden text|| (14:33) Spoiler: ||hidden text|| │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) [Фото] (14:33) [Фото] │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) Popular message (14:33) Popular message │
│[👍 ] 5 👎 3 │ │[👍 ] 5 👎 3 │
│ │ │ │
│ │ │ │

View File

@@ -10,7 +10,7 @@ expression: output
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│┌ Mom: Original message text │ │┌ Mom: Original message text │
│ (14:33) This is a reply (14:33) This is a reply │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) Selected message (14:33) Selected message │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,11 +9,11 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│Alice ──────────────── │ │Alice ──────────────── │
│ (14:33) First message (14:33) First message │
│ (14:33) Second message (14:33) Second message │
│ │ │ │
│Bob ──────────────── │ │Bob ──────────────── │
│ (14:33) Third message (14:33) Third message │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│Mom ──────────────── │ │Mom ──────────────── │
│ (14:33) Hello there! (14:33) Hello there! │
│ │ │ │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) Great! (14:33) Great! │
│[👍 ] │ │[👍 ] │
│ │ │ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) React to this (14:33) React to this │
│ │ │ │
│ ┌ Выбери реакцию ────────────────────────────────┐ │ │ ┌ Выбери реакцию ────────────────────────────────┐ │
│ │ │ │ │ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) React to this (14:33) React to this │
│ │ │ │
│ ┌ Выбери реакцию ────────────────────────────────┐ │ │ ┌ Выбери реакцию ────────────────────────────────┐ │
│ │ │ │ │ │ │ │

View File

@@ -10,7 +10,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │ │ ──────── 02.01.2022 ──────── │
│ │ │ │
│User ──────────────── │ │User ──────────────── │
│ (14:33) Regular message (14:33) Regular message │
│ │ │ │
│ │ │ │
│ │ │ │