mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-01 09:00:16 +00:00
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:
parent
39dd9efd5d
commit
ca09c503ca
@ -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,
|
||||
|
@ -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]]
|
||||
|
@ -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]]
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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: ®ex::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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: ®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
|
||||
@ -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)]
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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>(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
tests/app.rs
14
tests/app.rs
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user