diff --git a/TODO.md b/TODO.md index 0879655..d04b7e1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ - [x] bulk category editing: when on a certain category view, add bulk category editing, such that all channels saved to that category are updated with the new categories set. -- [ ] ui improvement: display the pinned status on category tab titles for pinned categories +- [x] ui improvement: display the pinned status on category tab titles for pinned categories - [x] start the terebi player as a new instance explicitly: ```sh terebi --new-instance diff --git a/src/main.rs b/src/main.rs index 2a26c1f..4bc9dae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,8 @@ //! Esc returns to NORMAL without clearing the filter; another Esc closes. Search input editing shortcuts apply. //! Matching selections survive filter edits; no matches leaves Enter/p without an action. //! Pinned categories stay visible; browsing another category replaces the single temporary tab. +//! Pinned category tab titles use `[p]`, matching the selector; Search, Feed, and temporary tabs do not. +//! The marker shares selection styling and counts toward whole-label wrapping without changing category names. //! Browsing a pinned category focuses its existing tab and removes the temporary tab. //! Top-level `pinned_categories = ["Favorites"]` persists pins; missing category names are ignored. //! Tab/Shift-Tab cycle only visible tabs; hidden views retain cached rows and selection. @@ -1213,7 +1215,8 @@ impl App { let mut selected_row = 0; for (index, view) in self.visible_views() { let mut title = Span::raw(view.name.as_str()); - let title_width = title.width() + 2; // One space of padding on each side. + let pinned = index >= 2 && self.config.pinned_categories.contains(&view.name); + let title_width = title.width() + 2 + if pinned { 4 } else { 0 }; // Padding and pin marker. if row_width != 0 && row_width + 1 + title_width > usize::from(area.width) { tab_lines.push(Line::default()); row_width = 0; @@ -1226,7 +1229,11 @@ impl App { if row_width != 0 { row.spans.push("│".into()); } - row.spans.extend([" ".into(), title, " ".into()]); + row.spans.push(" ".into()); + if pinned { + row.spans.push(Span::styled("[p] ", title.style)); + } + row.spans.extend([title, " ".into()]); row_width += usize::from(row_width != 0) + title_width; } // Leave space for search, a usable results pane, status, and help. @@ -2175,39 +2182,49 @@ mod tests { .collect(); app.view_index = 4; app.mode = Mode::Insert; - for (width, height, language, japanese, chinese, cursor_y) in [ - (30, 20, Some((1, 1)), (1, 2), (14, 2), 4), - (56, 20, Some((17, 0)), (38, 0), (51, 0), 2), - (55, 20, Some((17, 0)), (38, 0), (1, 1), 3), - (9, 20, None, (1, 3), (1, 4), 6), - (30, 12, Some((1, 0)), (1, 1), (14, 1), 3), - ] { + for (width, height) in [(30, 20), (56, 20), (55, 20), (9, 20), (30, 12)] { let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); terminal .draw(|frame| app.draw(frame, Duration::ZERO)) .unwrap(); + let tab_height = terminal.get_cursor_position().unwrap().y - 1; let buffer = terminal.backend().buffer(); - if let Some((x, y)) = language { - for (offset, character) in "Language immersion".chars().enumerate() { - assert_eq!( - buffer.cell((x + offset as u16, y)).unwrap().symbol(), - character.to_string(), - "whole label at width {width}, height {height}" - ); - } - } - assert_eq!(buffer.cell(japanese).unwrap().symbol(), "日"); - assert_ne!(buffer.cell(japanese).unwrap().fg, Color::Cyan); - assert_eq!(buffer.cell(chinese).unwrap().symbol(), "中"); + let locate = |symbol: &str| { + (0..tab_height) + .flat_map(|y| (0..width).map(move |x| (x, y))) + .find(|&position| buffer.cell(position).unwrap().symbol() == symbol) + }; + let chinese = locate("中").expect("selected category remains visible"); + assert_eq!(buffer.cell(chinese).unwrap().fg, Color::Cyan); assert_eq!( buffer.cell((chinese.0 + 2, chinese.1)).unwrap().symbol(), "文" ); - assert_eq!(buffer.cell(chinese).unwrap().fg, Color::Cyan); - assert_eq!( - terminal.get_cursor_position().unwrap(), - (1, cursor_y).into() - ); + if width >= 16 { + let japanese = locate("日").expect("neighboring category remains visible"); + for (offset, character) in "日本語だけ".chars().enumerate() { + assert_eq!( + buffer + .cell((japanese.0 + offset as u16 * 2, japanese.1)) + .unwrap() + .symbol(), + character.to_string(), + "category label must not split across rows" + ); + } + assert_ne!(buffer.cell(japanese).unwrap().fg, Color::Cyan); + } + if width >= 24 { + assert!( + (0..tab_height).any(|y| { + (0..width) + .map(|x| buffer.cell((x, y)).unwrap().symbol()) + .collect::() + .contains("Language immersion") + }), + "whole category label must fit after wrapping" + ); + } } }