mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 03:25:23 +00:00
feat(cable): support for custom preview offset parsing for cable channels
This commit is contained in:
parent
39dd9efd5d
commit
ef2842a395
@ -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]]
|
||||
|
@ -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<String>,
|
||||
@ -94,8 +96,27 @@ impl Channel {
|
||||
|
||||
pub fn get_result(&self, index: u32) -> Option<Entry> {
|
||||
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::<usize>().unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Entry::new(name)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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> = 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::<Vec<&str>>();
|
||||
|
||||
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::<usize>().unwrap();
|
||||
format!("'{}'", parts[index])
|
||||
})
|
||||
.to_string();
|
||||
|
||||
formatted_command
|
||||
format_prototype_string(&self.command, &entry.name, &self.delimiter)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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> = regex!(r"\{(\d+)\}");
|
||||
|
||||
pub fn format_prototype_string(
|
||||
template: &str,
|
||||
source: &str,
|
||||
delimiter: &str,
|
||||
) -> String {
|
||||
let parts = source.split(delimiter).collect::<Vec<&str>>();
|
||||
|
||||
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::<usize>().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
|
||||
|
@ -25,6 +25,14 @@ pub struct Cli {
|
||||
#[arg(short, long, value_name = "STRING", verbatim_doc_comment)]
|
||||
pub preview: Option<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// Disable the preview panel entirely on startup.
|
||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||
pub no_preview: bool,
|
||||
|
@ -75,8 +75,7 @@ impl From<Cli> 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;
|
||||
|
@ -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<u16>,
|
||||
) {
|
||||
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::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let target_line: Option<u16> =
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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<u16>,
|
||||
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>(
|
||||
|
@ -416,10 +416,7 @@ impl Television {
|
||||
// available previews
|
||||
let entry = selected_entry.as_ref().unwrap();
|
||||
if let Ok(preview) = receiver.try_recv() {
|
||||
self.preview_state.update(
|
||||
preview,
|
||||
// scroll to center the selected entry
|
||||
entry
|
||||
let scroll = entry
|
||||
.line_number
|
||||
.unwrap_or(0)
|
||||
.saturating_sub(
|
||||
@ -427,14 +424,18 @@ impl Television {
|
||||
.ui_state
|
||||
.layout
|
||||
.preview_window
|
||||
.map_or(0, |w| w.height)
|
||||
.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),
|
||||
.unwrap_or(0);
|
||||
self.preview_state.update(
|
||||
preview,
|
||||
scroll,
|
||||
entry.line_number.and_then(|l| l.try_into().ok()),
|
||||
);
|
||||
self.action_tx.send(Action::Render)?;
|
||||
|
Loading…
x
Reference in New Issue
Block a user