mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45: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]]
|
[[cable_channel]]
|
||||||
name = "text"
|
name = "text"
|
||||||
source_command = "rg . --no-heading --line-number"
|
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_delimiter = ":"
|
||||||
|
preview_offset = "{1}"
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
[[cable_channel]]
|
[[cable_channel]]
|
||||||
|
@ -14,6 +14,8 @@ use crate::matcher::Matcher;
|
|||||||
use crate::matcher::{config::Config, injector::Injector};
|
use crate::matcher::{config::Config, injector::Injector};
|
||||||
use crate::utils::command::shell_command;
|
use crate::utils::command::shell_command;
|
||||||
|
|
||||||
|
use super::prototypes::format_prototype_string;
|
||||||
|
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
matcher: Matcher<String>,
|
matcher: Matcher<String>,
|
||||||
@ -94,8 +96,27 @@ impl Channel {
|
|||||||
|
|
||||||
pub fn get_result(&self, index: u32) -> Option<Entry> {
|
pub fn get_result(&self, index: u32) -> Option<Entry> {
|
||||||
self.matcher.get_result(index).map(|item| {
|
self.matcher.get_result(index).map(|item| {
|
||||||
let path = item.matched_string;
|
let name = item.matched_string;
|
||||||
Entry::new(path)
|
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::{
|
use crate::channels::{
|
||||||
entry::Entry,
|
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)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||||
pub struct PreviewCommand {
|
pub struct PreviewCommand {
|
||||||
@ -47,23 +45,7 @@ impl PreviewCommand {
|
|||||||
/// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'");
|
/// assert_eq!(formatted_command, "something 'a:given:entry:to:preview' 'entry' 'a'");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn format_with(&self, entry: &Entry) -> String {
|
pub fn format_with(&self, entry: &Entry) -> String {
|
||||||
let parts = entry.name.split(&self.delimiter).collect::<Vec<&str>>();
|
format_prototype_string(&self.command, &entry.name, &self.delimiter)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use lazy_regex::{regex, Lazy, Regex};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Display, Formatter},
|
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.
|
/// A neat `HashMap` of channel prototypes indexed by their name.
|
||||||
///
|
///
|
||||||
/// This is used to store cable channel prototypes throughout the application
|
/// 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)]
|
#[arg(short, long, value_name = "STRING", verbatim_doc_comment)]
|
||||||
pub preview: Option<String>,
|
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.
|
/// Disable the preview panel entirely on startup.
|
||||||
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
#[arg(long, default_value = "false", verbatim_doc_comment)]
|
||||||
pub no_preview: bool,
|
pub no_preview: bool,
|
||||||
|
@ -75,8 +75,7 @@ impl From<Cli> for PostProcessedCli {
|
|||||||
let preview_command = cli.preview.map(|preview| PreviewCommand {
|
let preview_command = cli.preview.map(|preview| PreviewCommand {
|
||||||
command: preview,
|
command: preview,
|
||||||
delimiter: cli.delimiter.clone(),
|
delimiter: cli.delimiter.clone(),
|
||||||
// TODO: add the --preview-offset option to the CLI
|
offset_expr: cli.preview_offset.clone(),
|
||||||
offset_expr: None,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut channel: ChannelPrototype;
|
let mut channel: ChannelPrototype;
|
||||||
|
@ -9,7 +9,7 @@ pub struct PreviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PREVIEW_MIN_SCROLL_LINES: u16 = 3;
|
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;
|
const ANSI_CONTEXT_SIZE: usize = 500;
|
||||||
|
|
||||||
impl PreviewState {
|
impl PreviewState {
|
||||||
@ -51,7 +51,7 @@ impl PreviewState {
|
|||||||
scroll: u16,
|
scroll: u16,
|
||||||
target_line: Option<u16>,
|
target_line: Option<u16>,
|
||||||
) {
|
) {
|
||||||
if self.preview.title != preview.title {
|
if self.preview.title != preview.title || self.scroll != scroll {
|
||||||
self.preview = preview;
|
self.preview = preview;
|
||||||
self.scroll = scroll;
|
self.scroll = scroll;
|
||||||
self.target_line = target_line;
|
self.target_line = target_line;
|
||||||
@ -59,16 +59,31 @@ impl PreviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_render_context(&self) -> Self {
|
pub fn for_render_context(&self) -> Self {
|
||||||
let skipped_lines =
|
let num_skipped_lines =
|
||||||
self.scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE);
|
self.scroll.saturating_sub(ANSI_BEFORE_CONTEXT_SIZE);
|
||||||
let cropped_content = self
|
let cropped_content = self
|
||||||
.preview
|
.preview
|
||||||
.content
|
.content
|
||||||
.lines()
|
.lines()
|
||||||
.skip(skipped_lines as usize)
|
.skip(num_skipped_lines as usize)
|
||||||
.take(ANSI_CONTEXT_SIZE)
|
.take(ANSI_CONTEXT_SIZE)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.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(
|
PreviewState::new(
|
||||||
self.enabled,
|
self.enabled,
|
||||||
Preview::new(
|
Preview::new(
|
||||||
@ -77,8 +92,8 @@ impl PreviewState {
|
|||||||
self.preview.icon,
|
self.preview.icon,
|
||||||
self.preview.total_lines,
|
self.preview.total_lines,
|
||||||
),
|
),
|
||||||
skipped_lines,
|
num_skipped_lines,
|
||||||
self.target_line,
|
target_line,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,13 +32,19 @@ pub fn draw_preview_content_block(
|
|||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
)?;
|
)?;
|
||||||
// render the preview content
|
// 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);
|
f.render_widget(rp, inner);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_preview_paragraph(content: &str) -> Paragraph<'_> {
|
pub fn build_preview_paragraph(
|
||||||
|
preview_state: &PreviewState,
|
||||||
|
highlight_bg: Color,
|
||||||
|
) -> Paragraph<'_> {
|
||||||
let preview_block =
|
let preview_block =
|
||||||
Block::default().style(Style::default()).padding(Padding {
|
Block::default().style(Style::default()).padding(Padding {
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -47,14 +53,30 @@ pub fn build_preview_paragraph(content: &str) -> Paragraph<'_> {
|
|||||||
left: 1,
|
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>(
|
fn build_ansi_text_paragraph<'a>(
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
preview_block: Block<'a>,
|
preview_block: Block<'a>,
|
||||||
|
target_line: Option<u16>,
|
||||||
|
highlight_bg: Color,
|
||||||
) -> Paragraph<'a> {
|
) -> 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>(
|
pub fn build_meta_preview_paragraph<'a>(
|
||||||
|
@ -416,10 +416,7 @@ impl Television {
|
|||||||
// available previews
|
// available previews
|
||||||
let entry = selected_entry.as_ref().unwrap();
|
let entry = selected_entry.as_ref().unwrap();
|
||||||
if let Ok(preview) = receiver.try_recv() {
|
if let Ok(preview) = receiver.try_recv() {
|
||||||
self.preview_state.update(
|
let scroll = entry
|
||||||
preview,
|
|
||||||
// scroll to center the selected entry
|
|
||||||
entry
|
|
||||||
.line_number
|
.line_number
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
.saturating_sub(
|
.saturating_sub(
|
||||||
@ -427,14 +424,18 @@ impl Television {
|
|||||||
.ui_state
|
.ui_state
|
||||||
.layout
|
.layout
|
||||||
.preview_window
|
.preview_window
|
||||||
.map_or(0, |w| w.height)
|
.map_or(0, |w| w.height.saturating_sub(2)) // borders
|
||||||
/ 2)
|
/ 2)
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
|
.saturating_add(3) // 3 lines above the center
|
||||||
.try_into()
|
.try_into()
|
||||||
// if the scroll doesn't fit in a u16, just scroll to the top
|
// if the scroll doesn't fit in a u16, just scroll to the top
|
||||||
// this is a current limitation of ratatui
|
// 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()),
|
entry.line_number.and_then(|l| l.try_into().ok()),
|
||||||
);
|
);
|
||||||
self.action_tx.send(Action::Render)?;
|
self.action_tx.send(Action::Render)?;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user