From d72782a96fb70d0b29276189f732fd82f1cb7a8d Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 11 Oct 2026 11:13:27 +0200 Subject: [PATCH] feat: show dimmed video metadata beside thumbnails --- src/main.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index cbb89ad..334dbb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,8 @@ //! Esc closes the popup; j/k loads the selected video's description and i returns to editing. //! Normal-mode t toggles a thumbnail in the same-sized popup; j/k follows selection, Esc closes. //! Images use native terminal graphics when supported, otherwise colored half-block characters. -//! Thumbnails scale up or down proportionally to fill the popup without cropping. +//! Thumbnails scale proportionally in the left pane without cropping. +//! The right pane shows a wrapped title above author · publication date, dimmed like the footer. //! curl fetches JPEGs lazily into memory; decoding and image resizing/encoding run off-thread. //! Channel tags live in `$XDG_CONFIG_HOME/tubular/config.toml`, or `~/.config/tubular/config.toml`. //! `[categories]` stores flat, overlapping tags: one channel can appear in several arrays. @@ -1021,15 +1022,39 @@ impl App { { let popup = preview_area(frame.area()); frame.render_widget(Clear, popup); - let block = Block::bordered() - .title_bottom(Line::from(vec![ - " ".into(), - video.title.as_str().into(), - " ".into(), - ])) - .border_style(Style::new().fg(Color::Cyan)); + let block = Block::bordered().border_style(Style::new().fg(Color::Cyan)); + let [image, _, mut details] = Layout::horizontal([ + Constraint::Fill(3), + Constraint::Length(2), + Constraint::Fill(2), + ]) + .areas(block.inner(popup)); + let metadata = Paragraph::new(vec![ + Line::raw(video.title.as_str()), + Line::from(vec![ + video.channel.as_deref().unwrap_or("Unknown").into(), + " · ".into(), + video + .publication_date + .as_deref() + .unwrap_or("Unknown") + .into(), + ]), + ]) + .wrap(Wrap { trim: true }) + .dim(); + let height = if details.is_empty() { + 0 + } else { + metadata + .line_count(details.width) + .min(details.height as usize) as u16 + }; + details.y += (details.height - height) / 2; + details.height = height; + frame.render_widget(metadata, details); // Drawing polls completion and starts resize work before we choose the title state. - self.thumbnail.draw(frame, block.inner(popup)); + self.thumbnail.draw(frame, image); frame.render_widget( block.title(loading_title( " Thumbnail ", @@ -1234,6 +1259,39 @@ mod tests { ) } + #[test] + fn thumbnail_title_wraps_above_a_dimmed_byline_in_the_right_pane() { + use ratatui::{Terminal, backend::TestBackend, style::Modifier}; + + let mut app = app(&["tubular"]); + app.views[0].videos = youtube::search_results( + r#"{"entries":[{"id":"jNQXAC9IVRw","title":"First second third fourth fifth","channel":"Creator","upload_date":"20050423"}]}"#, + ).unwrap().videos; + app.views[0].selection.select(Some(0)); + app.thumbnail_open = true; + for (width, height, x, title_y, title_lines) in [ + (80, 24, 46, 10, vec!["First second third", "fourth fifth"]), + (120, 40, 69, 19, vec!["First second third fourth fifth"]), + ] { + let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); + terminal + .draw(|frame| app.draw(frame, Duration::ZERO)) + .unwrap(); + let buffer = terminal.backend().buffer(); + let mut lines = title_lines; + lines.push("Creator · 2005-04-23"); + for (row, text) in lines.iter().enumerate() { + for (column, character) in text.chars().enumerate() { + let cell = buffer + .cell((x + column as u16, title_y + row as u16)) + .unwrap(); + assert_eq!(cell.symbol(), character.to_string()); + assert!(cell.modifier.contains(Modifier::DIM)); + } + } + } + } + #[test] fn view_tabs_wrap_whole_labels_and_keep_the_selected_row_visible() { use ratatui::{Terminal, backend::TestBackend};