From ef2842a395d97488bcf0c48d2f0271b8fa46dbb4 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier Date: Thu, 15 May 2025 01:49:57 +0200 Subject: [PATCH] feat(cable): support for custom preview offset parsing for cable channels --- cable/unix-channels.toml | 3 ++- television/channels/cable.rs | 25 ++++++++++++++++++++-- television/channels/preview.rs | 26 ++++------------------- television/channels/prototypes.rs | 24 +++++++++++++++++++++ television/cli/args.rs | 8 +++++++ television/cli/mod.rs | 3 +-- television/previewer/state.rs | 27 ++++++++++++++++++------ television/screen/preview.rs | 30 ++++++++++++++++++++++---- television/television.rs | 35 ++++++++++++++++--------------- 9 files changed, 127 insertions(+), 54 deletions(-) diff --git a/cable/unix-channels.toml b/cable/unix-channels.toml index b98b885..4591b0e 100644 --- a/cable/unix-channels.toml +++ b/cable/unix-channels.toml @@ -8,8 +8,9 @@ preview_command = "bat -n --color=always {}" [[cable_channel]] name = "text" source_command = "rg . --no-heading --line-number" -preview_command = "bat -n --color=always {0} -H {1}" +preview_command = "bat -n --color=always {0}" preview_delimiter = ":" +preview_offset = "{1}" # Directories [[cable_channel]] diff --git a/television/channels/cable.rs b/television/channels/cable.rs index 1597eff..7a906da 100644 --- a/television/channels/cable.rs +++ b/television/channels/cable.rs @@ -14,6 +14,8 @@ use crate::matcher::Matcher; use crate::matcher::{config::Config, injector::Injector}; use crate::utils::command::shell_command; +use super::prototypes::format_prototype_string; + pub struct Channel { pub name: String, matcher: Matcher, @@ -94,8 +96,27 @@ impl Channel { pub fn get_result(&self, index: u32) -> Option { self.matcher.get_result(index).map(|item| { - let path = item.matched_string; - Entry::new(path) + let name = item.matched_string; + if let Some(cmd) = &self.preview_command { + if let Some(offset_expr) = &cmd.offset_expr { + let offset_string = format_prototype_string( + offset_expr, + &name, + &cmd.delimiter, + ); + let offset_str = { + offset_string + .strip_prefix('\'') + .and_then(|s| s.strip_suffix('\'')) + .unwrap_or(&offset_string) + }; + + return Entry::new(name).with_line_number( + offset_str.parse::().unwrap(), + ); + } + } + Entry::new(name) }) } diff --git a/television/channels/preview.rs b/television/channels/preview.rs index 5c7f48a..328d13b 100644 --- a/television/channels/preview.rs +++ b/television/channels/preview.rs @@ -2,12 +2,10 @@ use std::fmt::Display; use crate::channels::{ entry::Entry, - prototypes::{ChannelPrototype, DEFAULT_DELIMITER}, + prototypes::{ + format_prototype_string, ChannelPrototype, DEFAULT_DELIMITER, + }, }; -use lazy_regex::{regex, Lazy, Regex}; -use tracing::debug; - -static CMD_RE: &Lazy = regex!(r"\{(\d+)\}"); #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] pub struct PreviewCommand { @@ -47,23 +45,7 @@ impl PreviewCommand { /// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'"); /// ``` pub fn format_with(&self, entry: &Entry) -> String { - let parts = entry.name.split(&self.delimiter).collect::>(); - - let mut formatted_command = self - .command - .replace("{}", format!("'{}'", entry.name).as_str()); - debug!("FORMATTED_COMMAND: {formatted_command}"); - debug!("PARTS: {parts:?}"); - - formatted_command = CMD_RE - .replace_all(&formatted_command, |caps: ®ex::Captures| { - let index = - caps.get(1).unwrap().as_str().parse::().unwrap(); - format!("'{}'", parts[index]) - }) - .to_string(); - - formatted_command + format_prototype_string(&self.command, &entry.name, &self.delimiter) } } diff --git a/television/channels/prototypes.rs b/television/channels/prototypes.rs index 57aae72..3cdc961 100644 --- a/television/channels/prototypes.rs +++ b/television/channels/prototypes.rs @@ -1,3 +1,4 @@ +use lazy_regex::{regex, Lazy, Regex}; use rustc_hash::FxHashMap; use std::{ fmt::{self, Display, Formatter}, @@ -126,6 +127,29 @@ impl Display for ChannelPrototype { } } +pub static CMD_RE: &Lazy = regex!(r"\{(\d+)\}"); + +pub fn format_prototype_string( + template: &str, + source: &str, + delimiter: &str, +) -> String { + let parts = source.split(delimiter).collect::>(); + + let mut formatted_string = + template.replace("{}", format!("'{}'", source).as_str()); + + formatted_string = CMD_RE + .replace_all(&formatted_string, |caps: ®ex::Captures| { + let index = + caps.get(1).unwrap().as_str().parse::().unwrap(); + format!("'{}'", parts[index]) + }) + .to_string(); + + formatted_string +} + /// A neat `HashMap` of channel prototypes indexed by their name. /// /// This is used to store cable channel prototypes throughout the application diff --git a/television/cli/args.rs b/television/cli/args.rs index f61d1d3..100bbe2 100644 --- a/television/cli/args.rs +++ b/television/cli/args.rs @@ -25,6 +25,14 @@ pub struct Cli { #[arg(short, long, value_name = "STRING", verbatim_doc_comment)] pub preview: Option, + /// A preview line number offset template to use to scroll the preview to for each + /// entry. + /// + /// This template uses the same syntax as the `preview` option and will be formatted + /// using the currently selected entry. + #[arg(long, value_name = "STRING", verbatim_doc_comment)] + pub preview_offset: Option, + /// Disable the preview panel entirely on startup. #[arg(long, default_value = "false", verbatim_doc_comment)] pub no_preview: bool, diff --git a/television/cli/mod.rs b/television/cli/mod.rs index 4b87808..dfa627c 100644 --- a/television/cli/mod.rs +++ b/television/cli/mod.rs @@ -75,8 +75,7 @@ impl From for PostProcessedCli { let preview_command = cli.preview.map(|preview| PreviewCommand { command: preview, delimiter: cli.delimiter.clone(), - // TODO: add the --preview-offset option to the CLI - offset_expr: None, + offset_expr: cli.preview_offset.clone(), }); let mut channel: ChannelPrototype; diff --git a/television/previewer/state.rs b/television/previewer/state.rs index d1265b2..57bfa91 100644 --- a/television/previewer/state.rs +++ b/television/previewer/state.rs @@ -9,7 +9,7 @@ pub struct PreviewState { } const PREVIEW_MIN_SCROLL_LINES: u16 = 3; -pub const ANSI_BEFORE_CONTEXT_SIZE: u16 = 10; +pub const ANSI_BEFORE_CONTEXT_SIZE: u16 = 3; const ANSI_CONTEXT_SIZE: usize = 500; impl PreviewState { @@ -51,7 +51,7 @@ impl PreviewState { scroll: u16, target_line: Option, ) { - if self.preview.title != preview.title { + if self.preview.title != preview.title || self.scroll != scroll { self.preview = preview; self.scroll = scroll; self.target_line = target_line; @@ -59,16 +59,31 @@ impl PreviewState { } pub fn for_render_context(&self) -> Self { - let skipped_lines = + let num_skipped_lines = self.scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE); let cropped_content = self .preview .content .lines() - .skip(skipped_lines as usize) + .skip(num_skipped_lines as usize) .take(ANSI_CONTEXT_SIZE) .collect::>() .join("\n"); + + let target_line: Option = + if let Some(target_line) = self.target_line { + if num_skipped_lines < target_line + && (target_line - num_skipped_lines) + <= u16::try_from(ANSI_CONTEXT_SIZE).unwrap() + { + Some(target_line.saturating_sub(num_skipped_lines)) + } else { + None + } + } else { + None + }; + PreviewState::new( self.enabled, Preview::new( @@ -77,8 +92,8 @@ impl PreviewState { self.preview.icon, self.preview.total_lines, ), - skipped_lines, - self.target_line, + num_skipped_lines, + target_line, ) } } diff --git a/television/screen/preview.rs b/television/screen/preview.rs index 87692be..de1ad1c 100644 --- a/television/screen/preview.rs +++ b/television/screen/preview.rs @@ -32,13 +32,19 @@ pub fn draw_preview_content_block( use_nerd_font_icons, )?; // render the preview content - let rp = build_preview_paragraph(&preview_state.preview.content); + let rp = build_preview_paragraph( + preview_state, + colorscheme.preview.highlight_bg, + ); f.render_widget(rp, inner); Ok(()) } -pub fn build_preview_paragraph(content: &str) -> Paragraph<'_> { +pub fn build_preview_paragraph( + preview_state: &PreviewState, + highlight_bg: Color, +) -> Paragraph<'_> { let preview_block = Block::default().style(Style::default()).padding(Padding { top: 0, @@ -47,14 +53,30 @@ pub fn build_preview_paragraph(content: &str) -> Paragraph<'_> { left: 1, }); - build_ansi_text_paragraph(content, preview_block) + build_ansi_text_paragraph( + &preview_state.preview.content, + preview_block, + preview_state.target_line, + highlight_bg, + ) } fn build_ansi_text_paragraph<'a>( text: &'a str, preview_block: Block<'a>, + target_line: Option, + highlight_bg: Color, ) -> Paragraph<'a> { - Paragraph::new(text.into_text().unwrap()).block(preview_block) + let mut t = text.into_text().unwrap(); + if let Some(target_line) = target_line { + // Highlight the target line + if let Some(line) = t.lines.get_mut((target_line - 1) as usize) { + for span in &mut line.spans { + span.style = span.style.bg(highlight_bg); + } + } + } + Paragraph::new(t).block(preview_block) } pub fn build_meta_preview_paragraph<'a>( diff --git a/television/television.rs b/television/television.rs index cc46b6f..efad841 100644 --- a/television/television.rs +++ b/television/television.rs @@ -416,25 +416,26 @@ impl Television { // available previews let entry = selected_entry.as_ref().unwrap(); if let Ok(preview) = receiver.try_recv() { + let scroll = entry + .line_number + .unwrap_or(0) + .saturating_sub( + (self + .ui_state + .layout + .preview_window + .map_or(0, |w| w.height.saturating_sub(2)) // borders + / 2) + .into(), + ) + .saturating_add(3) // 3 lines above the center + .try_into() + // if the scroll doesn't fit in a u16, just scroll to the top + // this is a current limitation of ratatui + .unwrap_or(0); self.preview_state.update( preview, - // scroll to center the selected entry - entry - .line_number - .unwrap_or(0) - .saturating_sub( - (self - .ui_state - .layout - .preview_window - .map_or(0, |w| w.height) - / 2) - .into(), - ) - .try_into() - // if the scroll doesn't fit in a u16, just scroll to the top - // this is a current limitation of ratatui - .unwrap_or(0), + scroll, entry.line_number.and_then(|l| l.try_into().ok()), ); self.action_tx.send(Action::Render)?;