refactor!(cable): update cable channel preview configuration format + add custom preview offsets (#511)

BREAKING CHANGE: the format of the cable channel files and more
specifically the preview specification is updated to be a single table
named `preview` (with keys `command`, `delimiter`, and `offset`) instead
of three flat fields.

```toml
preview_command = "echo 3"
preview_delimiter = " "
preview_offset = "{1}"
```

becomes: 

```toml
preview.command = "echo 3"
preview.delimiter = " "
preview.offset = "{1}"
```
This commit is contained in:
Alex Pasmantier 2025-05-16 01:07:12 +02:00 committed by GitHub
parent 39dd9efd5d
commit ca09c503ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 237 additions and 232 deletions

View File

@ -472,11 +472,11 @@ pub fn draw(c: &mut Criterion) {
let backend = TestBackend::new(width, height);
let terminal = Terminal::new(backend).unwrap();
let (tx, _) = tokio::sync::mpsc::unbounded_channel();
let channel = ChannelPrototype::default();
let channel_prototype = ChannelPrototype::default();
// Wait for the channel to finish loading
let mut tv = Television::new(
tx,
channel,
&channel_prototype,
config,
None,
false,

View File

@ -2,26 +2,27 @@
[[cable_channel]]
name = "files"
source_command = "fd -t f"
preview_command = "bat -n --color=always {}"
preview.command = "bat -n --color=always {}"
# Text
[[cable_channel]]
name = "text"
source_command = "rg . --no-heading --line-number"
preview_command = "bat -n --color=always {0} -H {1}"
preview_delimiter = ":"
preview.command = "bat -n --color=always {0}"
preview.delimiter = ":"
preview.offset = "{1}"
# Directories
[[cable_channel]]
name = "dirs"
source_command = "fd -t d"
preview_command = "ls -la --color=always {}"
preview.command = "ls -la --color=always {}"
# Environment variables
[[cable_channel]]
name = "env"
source_command = "printenv"
preview_command = "cut -d= -f2 <<< ${0} | cut -d\" \" -f2- | sed 's/:/\\n/g'"
preview.command = "cut -d= -f2 <<< ${0} | cut -d\" \" -f2- | sed 's/:/\\n/g'"
# Aliases
[[cable_channel]]
@ -32,47 +33,50 @@ interactive = true
# GIT
[[cable_channel]]
name = "git-repos"
# this is a MacOS version but feel free to change it to fit your needs
# this is a MacOS version but feel free to override it to fit your needs
source_command = "fd -g .git -HL -t d -d 10 --prune ~ -E 'Library' -E 'Application Support' --exec dirname {}"
preview_command = "cd {} && git log -n 200 --pretty=medium --all --graph --color"
preview.command = "cd {} && git log -n 200 --pretty=medium --all --graph --color"
[[cable_channel]]
name = "git-diff"
source_command = "git diff --name-only"
preview_command = "git diff --color=always {0}"
preview.command = "git diff --color=always {0}"
[[cable_channel]]
name = "git-reflog"
source_command = 'git reflog'
preview_command = 'git show -p --stat --pretty=fuller --color=always {0}'
preview.command = 'git show -p --stat --pretty=fuller --color=always {0}'
[[cable_channel]]
name = "git-log"
source_command = "git log --oneline --date=short --pretty=\"format:%h %s %an %cd\" \"$@\""
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
preview.command = 'git show -p --stat --pretty=fuller --color=always {0}'
[[cable_channel]]
name = "git-branch"
source_command = "git --no-pager branch --all --format=\"%(refname:short)\""
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
preview.command = 'git show -p --stat --pretty=fuller --color=always {0}'
# Docker
[[cable_channel]]
name = "docker-images"
source_command = "docker image list --format \"{{.ID}}\""
preview_command = "docker image inspect {0} | jq -C"
preview.command = "docker image inspect {0} | jq -C"
# S3
[[cable_channel]]
name = "s3-buckets"
source_command = "aws s3 ls | cut -d \" \" -f 3"
preview_command = "aws s3 ls s3://{0}"
preview.command = "aws s3 ls s3://{0}"
# Dotfiles
[[cable_channel]]
name = "my-dotfiles"
source_command = "fd -t f . $HOME/.config"
preview_command = "bat -n --color=always {}"
preview.command = "bat -n --color=always {}"
# Shell history
[[cable_channel]]

View File

@ -2,13 +2,13 @@
[[cable_channel]]
name = "files"
source_command = "Get-ChildItem -Recurse -File | Select-Object -ExpandProperty FullName"
preview_command = "bat -n --color=always {}"
preview.command = "bat -n --color=always {}"
# Directories
[[cable_channel]]
name = "dirs"
source_command = "Get-ChildItem -Recurse -Directory | Select-Object -ExpandProperty FullName"
preview_command = "ls -l {}"
preview.command = "ls -l {}"
# Environment variables
[[cable_channel]]
@ -24,39 +24,39 @@ source_command = "Get-Alias"
[[cable_channel]]
name = "git-repos"
source_command = "Get-ChildItem -Path 'C:\\Users' -Recurse -Directory -Force -ErrorAction SilentlyContinue | Where-Object { Test-Path \"$($_.FullName)\\.git\" } | Select-Object -ExpandProperty FullName"
preview_command = "cd '{}' ; git log -n 200 --pretty=medium --all --graph --color"
preview.command = "cd '{}' ; git log -n 200 --pretty=medium --all --graph --color"
[[cable_channel]]
name = "git-diff"
source_command = "git diff --name-only"
preview_command = "git diff --color=always {}"
preview.command = "git diff --color=always {}"
[[cable_channel]]
name = "git-reflog"
source_command = "git reflog"
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
preview.command = "git show -p --stat --pretty=fuller --color=always {0}"
[[cable_channel]]
name = "git-log"
source_command = "git log --oneline --date=short --pretty='format:%h %s %an %cd'"
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
preview.command = "git show -p --stat --pretty=fuller --color=always {0}"
[[cable_channel]]
name = "git-branch"
source_command = "git branch --all --format='%(refname:short)'"
preview_command = "git show -p --stat --pretty=fuller --color=always {0}"
preview.command = "git show -p --stat --pretty=fuller --color=always {0}"
# Docker
[[cable_channel]]
name = "docker-images"
source_command = "docker image ls --format '{{.ID}}'"
preview_command = "docker image inspect {0} | jq -C"
preview.command = "docker image inspect {0} | jq -C"
# Dotfiles (adapted to common Windows dotfile locations)
[[cable_channel]]
name = "my-dotfiles"
source_command = "Get-ChildItem -Recurse -File -Path \"$env:USERPROFILE\\AppData\\Roaming\\\""
preview_command = "bat -n --color=always {}"
preview.command = "bat -n --color=always {}"
# Shell history
[[cable_channel]]

View File

@ -137,7 +137,7 @@ const ACTION_BUF_SIZE: usize = 8;
impl App {
pub fn new(
channel_prototype: ChannelPrototype,
channel_prototype: &ChannelPrototype,
config: Config,
input: Option<String>,
options: AppOptions,

View File

@ -80,11 +80,10 @@ pub fn load_cable() -> Result<Cable> {
},
);
debug!(
"Loaded {} default and {} custom prototypes",
default_prototypes.prototypes.len(),
prototypes.len()
);
debug!("Loaded {} custom cable channels", prototypes.len());
if prototypes.is_empty() {
debug!("No custom cable channels found");
}
let mut cable_channels = FxHashMap::default();
// custom prototypes take precedence over default ones

View File

@ -6,14 +6,14 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
use tracing::debug;
use crate::channels::{
entry::Entry,
preview::PreviewCommand,
prototypes::{ChannelPrototype, DEFAULT_DELIMITER},
entry::Entry, preview::PreviewCommand, prototypes::ChannelPrototype,
};
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>,
@ -24,53 +24,28 @@ pub struct Channel {
impl Default for Channel {
fn default() -> Self {
Self::new(
Self::new(&ChannelPrototype::new(
"files",
"find . -type f",
false,
Some(PreviewCommand::new("cat {}", ":", None)),
)
}
}
impl From<ChannelPrototype> for Channel {
fn from(prototype: ChannelPrototype) -> Self {
Self::new(
&prototype.name,
&prototype.source_command,
prototype.interactive,
match prototype.preview_command {
Some(command) => Some(PreviewCommand::new(
&command,
&prototype
.preview_delimiter
.unwrap_or(DEFAULT_DELIMITER.to_string()),
prototype.preview_offset,
)),
None => None,
},
)
))
}
}
impl Channel {
pub fn new(
name: &str,
entries_command: &str,
interactive: bool,
preview_command: Option<PreviewCommand>,
) -> Self {
pub fn new(prototype: &ChannelPrototype) -> Self {
let matcher = Matcher::new(Config::default());
let injector = matcher.injector();
let crawl_handle = tokio::spawn(load_candidates(
entries_command.to_string(),
interactive,
prototype.source_command.to_string(),
prototype.interactive,
injector,
));
Self {
matcher,
preview_command,
name: name.to_string(),
preview_command: prototype.preview_command.clone(),
name: prototype.name.to_string(),
selected_entries: HashSet::with_hasher(FxBuildHasher),
crawl_handle,
}
@ -94,8 +69,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)
})
}

View File

@ -1,21 +1,27 @@
use std::fmt::Display;
use crate::channels::{
entry::Entry,
prototypes::{ChannelPrototype, DEFAULT_DELIMITER},
};
use lazy_regex::{regex, Lazy, Regex};
use tracing::debug;
use serde::Deserialize;
static CMD_RE: &Lazy<Regex> = regex!(r"\{(\d+)\}");
use crate::channels::{entry::Entry, prototypes::format_prototype_string};
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize)]
pub struct PreviewCommand {
pub command: String,
#[serde(default = "default_delimiter")]
pub delimiter: String,
#[serde(rename = "offset")]
pub offset_expr: Option<String>,
}
pub const DEFAULT_DELIMITER: &str = " ";
/// The default delimiter to use for the preview command to use to split
/// entries into multiple referenceable parts.
#[allow(clippy::unnecessary_wraps)]
fn default_delimiter() -> String {
DEFAULT_DELIMITER.to_string()
}
impl PreviewCommand {
pub fn new(
command: &str,
@ -47,43 +53,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: &regex::Captures| {
let index =
caps.get(1).unwrap().as_str().parse::<usize>().unwrap();
format!("'{}'", parts[index])
})
.to_string();
formatted_command
}
}
impl From<&ChannelPrototype> for Option<PreviewCommand> {
fn from(value: &ChannelPrototype) -> Self {
if let Some(command) = value.preview_command.as_ref() {
let delimiter = value
.preview_delimiter
.as_ref()
.map_or(DEFAULT_DELIMITER, |v| v);
let offset_expr = value.preview_offset.clone();
// FIXME: handle offset here (side note: we don't want to reparse the offset
// expression for each entry, so maybe just parse it once and try to store it
// as some sort of function we can call later on
Some(PreviewCommand::new(command, delimiter, offset_expr))
} else {
None
}
format_prototype_string(&self.command, &entry.name, &self.delimiter)
}
}

View File

@ -1,3 +1,4 @@
use lazy_regex::{regex, Lazy, Regex};
use rustc_hash::FxHashMap;
use std::{
fmt::{self, Display, Formatter},
@ -43,10 +44,8 @@ pub struct ChannelPrototype {
pub source_command: String,
#[serde(default)]
pub interactive: bool,
pub preview_command: Option<String>,
#[serde(default = "default_delimiter")]
pub preview_delimiter: Option<String>,
pub preview_offset: Option<String>,
#[serde(rename = "preview")]
pub preview_command: Option<PreviewCommand>,
}
const STDIN_CHANNEL_NAME: &str = "stdin";
@ -57,52 +56,27 @@ impl ChannelPrototype {
name: &str,
source_command: &str,
interactive: bool,
preview_command: Option<String>,
preview_delimiter: Option<String>,
preview_offset: Option<String>,
preview_command: Option<PreviewCommand>,
) -> Self {
Self {
name: name.to_string(),
source_command: source_command.to_string(),
interactive,
preview_command,
preview_delimiter,
preview_offset,
}
}
pub fn stdin(preview: Option<PreviewCommand>) -> Self {
match preview {
Some(PreviewCommand {
command,
delimiter,
offset_expr,
}) => Self {
name: STDIN_CHANNEL_NAME.to_string(),
source_command: STDIN_SOURCE_COMMAND.to_string(),
interactive: false,
preview_command: Some(command),
preview_delimiter: Some(delimiter),
preview_offset: offset_expr,
},
None => Self {
name: STDIN_CHANNEL_NAME.to_string(),
source_command: STDIN_SOURCE_COMMAND.to_string(),
interactive: false,
preview_command: None,
preview_delimiter: None,
preview_offset: None,
},
Self {
name: STDIN_CHANNEL_NAME.to_string(),
source_command: STDIN_SOURCE_COMMAND.to_string(),
interactive: false,
preview_command: preview,
}
}
pub fn preview_command(&self) -> Option<PreviewCommand> {
self.into()
}
}
const DEFAULT_PROTOTYPE_NAME: &str = "files";
pub const DEFAULT_DELIMITER: &str = " ";
pub const DEFAULT_PROTOTYPE_NAME: &str = "files";
impl Default for ChannelPrototype {
fn default() -> Self {
@ -113,19 +87,35 @@ impl Default for ChannelPrototype {
}
}
/// The default delimiter to use for the preview command to use to split
/// entries into multiple referenceable parts.
#[allow(clippy::unnecessary_wraps)]
fn default_delimiter() -> Option<String> {
Some(DEFAULT_DELIMITER.to_string())
}
impl Display for ChannelPrototype {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
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: &regex::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
@ -142,6 +132,16 @@ impl Deref for Cable {
}
}
impl Cable {
pub fn default_channel(&self) -> ChannelPrototype {
self.get(DEFAULT_PROTOTYPE_NAME)
.cloned()
.unwrap_or_else(|| {
panic!("Default channel '{DEFAULT_PROTOTYPE_NAME}' not found")
})
}
}
/// A default cable channels specification that is compiled into the
/// application.
#[cfg(unix)]

View File

@ -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,

View File

@ -11,7 +11,7 @@ use crate::{
prototypes::{Cable, ChannelPrototype},
},
cli::args::{Cli, Command},
config::{get_config_dir, get_data_dir, KeyBindings, DEFAULT_CHANNEL},
config::{get_config_dir, get_data_dir, KeyBindings},
};
pub mod args;
@ -75,23 +75,19 @@ 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;
let working_directory: Option<String>;
let cable_channels = cable::load_cable().unwrap_or_default();
let cable = cable::load_cable().unwrap_or_default();
if cli.channel.is_none() {
channel = cable_channels
.get(DEFAULT_CHANNEL)
.expect("Default channel not found in cable channels")
.clone();
channel = cable.default_channel();
working_directory = cli.working_directory;
} else {
let cli_channel = cli.channel.as_ref().unwrap().to_owned();
match parse_channel(&cli_channel, &cable_channels) {
match parse_channel(&cli_channel, &cable) {
Ok(p) => {
channel = p;
working_directory = cli.working_directory;
@ -102,12 +98,7 @@ impl From<Cli> for PostProcessedCli {
if cli.working_directory.is_none()
&& Path::new(&cli_channel).exists()
{
channel = cable_channels
.get(DEFAULT_CHANNEL)
.expect(
"Default channel not found in cable channels",
)
.clone();
channel = cable.default_channel();
working_directory = Some(cli.channel.unwrap().clone());
} else {
unknown_channel_exit(&cli.channel.unwrap());
@ -117,10 +108,9 @@ impl From<Cli> for PostProcessedCli {
}
}
// override the default previewer
if let Some(preview_cmd) = &preview_command {
channel.preview_command = Some(preview_cmd.command.clone());
channel.preview_delimiter = Some(preview_cmd.delimiter.clone());
channel.preview_offset.clone_from(&preview_cmd.offset_expr);
channel.preview_command = Some(preview_cmd.clone());
}
Self {
@ -317,7 +307,11 @@ mod tests {
let post_processed_cli: PostProcessedCli = cli.into();
let expected = ChannelPrototype {
preview_delimiter: Some(":".to_string()),
preview_command: Some(PreviewCommand {
command: "bat -n --color=always {}".to_string(),
delimiter: ":".to_string(),
offset_expr: None,
}),
..Default::default()
};

View File

@ -15,6 +15,8 @@ pub use themes::Theme;
use tracing::{debug, warn};
pub use ui::UiConfig;
use crate::channels::prototypes::DEFAULT_PROTOTYPE_NAME;
mod keybindings;
pub mod shell_integration;
mod themes;
@ -39,10 +41,8 @@ pub struct AppConfig {
pub default_channel: String,
}
pub const DEFAULT_CHANNEL: &str = "files";
fn default_channel() -> String {
DEFAULT_CHANNEL.to_string()
DEFAULT_PROTOTYPE_NAME.to_string()
}
impl Hash for AppConfig {

View File

@ -60,7 +60,7 @@ async fn main() -> Result<()> {
// determine the channel to use based on the CLI arguments and configuration
debug!("Determining channel...");
let channel = determine_channel(
let channel_prototype = determine_channel(
args.clone(),
&config,
is_readable_stdin(),
@ -77,8 +77,13 @@ async fn main() -> Result<()> {
args.no_help,
config.application.tick_rate,
);
let mut app =
App::new(channel, config, args.input, options, &cable_channels);
let mut app = App::new(
&channel_prototype,
config,
args.input,
options,
&cable_channels,
);
stdout().flush()?;
debug!("Running application...");
let output = app.run(stdout().is_terminal(), false).await?;
@ -217,7 +222,7 @@ mod tests {
&args,
&config,
true,
&ChannelPrototype::new("stdin", "cat", false, None, None, None),
&ChannelPrototype::new("stdin", "cat", false, None),
None,
);
}
@ -226,7 +231,7 @@ mod tests {
async fn test_determine_channel_autocomplete_prompt() {
let autocomplete_prompt = Some("cd".to_string());
let expected_channel =
ChannelPrototype::new("dirs", "ls {}", false, None, None, None);
ChannelPrototype::new("dirs", "ls {}", false, None);
let args = PostProcessedCli {
autocomplete_prompt,
..Default::default()
@ -258,8 +263,7 @@ mod tests {
#[tokio::test]
async fn test_determine_channel_standard_case() {
let channel =
ChannelPrototype::new("dirs", "", false, None, None, None);
let channel = ChannelPrototype::new("dirs", "", false, None);
let args = PostProcessedCli {
channel,
..Default::default()
@ -269,7 +273,7 @@ mod tests {
&args,
&config,
false,
&ChannelPrototype::new("dirs", "", false, None, None, None),
&ChannelPrototype::new("dirs", "", false, None),
None,
);
}

View File

@ -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,
)
}
}

View File

@ -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>(

View File

@ -31,6 +31,7 @@ use std::collections::HashSet;
use tokio::sync::mpsc::{
unbounded_channel, UnboundedReceiver, UnboundedSender,
};
use tracing::debug;
#[derive(PartialEq, Copy, Clone, Hash, Eq, Debug, Serialize, Deserialize)]
pub enum Mode {
@ -72,7 +73,7 @@ impl Television {
#[must_use]
pub fn new(
action_tx: UnboundedSender<Action>,
channel_prototype: ChannelPrototype,
channel_prototype: &ChannelPrototype,
mut config: Config,
input: Option<String>,
no_remote: bool,
@ -86,9 +87,9 @@ impl Television {
}
// previewer
let preview_handles = Self::setup_previewer(&channel_prototype);
let preview_handles = Self::setup_previewer(channel_prototype);
let mut channel: CableChannel = channel_prototype.into();
let mut channel = CableChannel::new(channel_prototype);
let app_metadata = AppMetadata::new(
env!("CARGO_PKG_VERSION").to_string(),
@ -157,7 +158,7 @@ impl Television {
let (pv_request_tx, pv_request_rx) = unbounded_channel();
let (pv_preview_tx, pv_preview_rx) = unbounded_channel();
let previewer = Previewer::new(
channel_prototype.preview_command().unwrap(),
channel_prototype.preview_command.clone().unwrap(),
PreviewerConfig::default(),
pv_request_rx,
pv_preview_tx,
@ -204,7 +205,7 @@ impl Television {
self.channel.name.clone()
}
pub fn change_channel(&mut self, channel_prototype: ChannelPrototype) {
pub fn change_channel(&mut self, channel_prototype: &ChannelPrototype) {
self.preview_state.reset();
self.preview_state.enabled =
channel_prototype.preview_command.is_some();
@ -217,8 +218,9 @@ impl Television {
.send(PreviewRequest::Shutdown)
.expect("Failed to send shutdown signal to previewer");
}
self.preview_handles = Self::setup_previewer(&channel_prototype);
self.channel = channel_prototype.into();
self.preview_handles = Self::setup_previewer(channel_prototype);
self.channel = CableChannel::new(channel_prototype);
debug!("Changed channel to {:?}", channel_prototype);
}
pub fn find(&mut self, pattern: &str) {
@ -416,25 +418,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)?;
@ -547,7 +550,7 @@ impl Television {
self.reset_picker_input();
self.remote_control.as_mut().unwrap().find(EMPTY_STRING);
self.mode = Mode::Channel;
self.change_channel(new_channel);
self.change_channel(&new_channel);
}
}
}

View File

@ -47,7 +47,7 @@ fn setup_app(
false,
config.application.tick_rate,
);
let mut app = App::new(chan, config, input, options, &Cable::default());
let mut app = App::new(&chan, config, input, options, &Cable::default());
// retrieve the app's action channel handle in order to send a quit action
let tx = app.action_tx.clone();
@ -212,14 +212,8 @@ async fn test_app_exact_search_positive() {
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_app_exits_when_select_1_and_only_one_result() {
let prototype = ChannelPrototype::new(
"some_channel",
"echo file1.txt",
false,
None,
None,
None,
);
let prototype =
ChannelPrototype::new("some_channel", "echo file1.txt", false, None);
let (f, tx) = setup_app(Some(prototype), true, false);
// tick a few times to get the results
@ -255,8 +249,6 @@ async fn test_app_does_not_exit_when_select_1_and_more_than_one_result() {
"echo 'file1.txt\nfile2.txt'",
false,
None,
None,
None,
);
let (f, tx) = setup_app(Some(prototype), true, false);