mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
feat: add --print-current, --output-format (closes #981)
This commit is contained in:
parent
5c11314bb6
commit
d7117fbbc2
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -222,9 +222,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
|
|
|
|||
|
|
@ -132,8 +132,12 @@ fn sk_main(mut opts: SkimOptions) -> Result<i32> {
|
|||
print_cmd: opts.print_cmd,
|
||||
print_score: opts.print_score,
|
||||
print_header: opts.print_header,
|
||||
print_current: opts.print_current,
|
||||
output_ending: String::from(if opts.print0 { "\0" } else { "\n" }),
|
||||
strip_ansi: opts.ansi && !opts.no_strip_ansi,
|
||||
output_format: opts.output_format.clone(),
|
||||
delimiter: opts.delimiter.clone(),
|
||||
replstr: opts.replstr.clone(),
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
|
@ -159,38 +163,62 @@ fn sk_main(mut opts: SkimOptions) -> Result<i32> {
|
|||
return Ok(130);
|
||||
}
|
||||
|
||||
// output query
|
||||
if bin_options.print_query {
|
||||
print!("{}{}", result.query, bin_options.output_ending);
|
||||
}
|
||||
|
||||
if bin_options.print_cmd {
|
||||
print!("{}{}", result.cmd, bin_options.output_ending);
|
||||
}
|
||||
|
||||
if bin_options.print_header {
|
||||
print!("{}{}", result.header, bin_options.output_ending);
|
||||
}
|
||||
|
||||
if let Event::Action(Action::Accept(Some(accept_key))) = result.final_event {
|
||||
print!("{}{}", accept_key, bin_options.output_ending);
|
||||
}
|
||||
|
||||
for item in &result.selected_items {
|
||||
if bin_options.strip_ansi {
|
||||
print!(
|
||||
"{}{}",
|
||||
skim::helper::item::strip_ansi(&item.output()).0,
|
||||
bin_options.output_ending
|
||||
);
|
||||
} else {
|
||||
print!("{}{}", item.output(), bin_options.output_ending);
|
||||
// Output
|
||||
if let Some(ref output_format) = bin_options.output_format {
|
||||
print!(
|
||||
"{}{}",
|
||||
skim::printf(
|
||||
output_format,
|
||||
&bin_options.delimiter,
|
||||
&bin_options.replstr,
|
||||
result.selected_items.iter().map(|x| x.item.clone()),
|
||||
result.current,
|
||||
&result.query,
|
||||
&result.cmd,
|
||||
true
|
||||
),
|
||||
bin_options.output_ending
|
||||
);
|
||||
} else {
|
||||
if bin_options.print_query {
|
||||
print!("{}{}", result.query, bin_options.output_ending);
|
||||
}
|
||||
if bin_options.print_score {
|
||||
print!("{}{}", item.rank[0], bin_options.output_ending);
|
||||
|
||||
if bin_options.print_cmd {
|
||||
print!("{}{}", result.cmd, bin_options.output_ending);
|
||||
}
|
||||
|
||||
if bin_options.print_header {
|
||||
print!("{}{}", result.header, bin_options.output_ending);
|
||||
}
|
||||
|
||||
if bin_options.print_current {
|
||||
if let Some(ref current) = result.current {
|
||||
print!("{}{}", current.output(), bin_options.output_ending);
|
||||
} else {
|
||||
print!("{}", bin_options.output_ending);
|
||||
}
|
||||
}
|
||||
|
||||
if let Event::Action(Action::Accept(Some(accept_key))) = result.final_event {
|
||||
print!("{}{}", accept_key, bin_options.output_ending);
|
||||
}
|
||||
|
||||
for item in &result.selected_items {
|
||||
if bin_options.strip_ansi {
|
||||
print!(
|
||||
"{}{}",
|
||||
skim::helper::item::strip_ansi(&item.output()).0,
|
||||
bin_options.output_ending
|
||||
);
|
||||
} else {
|
||||
print!("{}{}", item.output(), bin_options.output_ending);
|
||||
}
|
||||
if bin_options.print_score {
|
||||
print!("{}{}", item.rank[0], bin_options.output_ending);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
|
@ -243,5 +271,9 @@ pub struct BinOptions {
|
|||
print_cmd: bool,
|
||||
print_score: bool,
|
||||
print_header: bool,
|
||||
print_current: bool,
|
||||
strip_ansi: bool,
|
||||
output_format: Option<String>,
|
||||
delimiter: regex::Regex,
|
||||
replstr: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ pub use crate::output::SkimOutput;
|
|||
pub use crate::skim::*;
|
||||
pub use crate::skim_item::SkimItem;
|
||||
use crate::tui::Size;
|
||||
pub use util::printf;
|
||||
|
||||
pub mod binds;
|
||||
mod engine;
|
||||
|
|
|
|||
|
|
@ -599,6 +599,16 @@ pub struct SkimOptions {
|
|||
#[cfg_attr(feature = "cli", arg(long, help_heading = "Scripting"))]
|
||||
pub print_header: bool,
|
||||
|
||||
/// Print the current (highlighted) item as the first line (after print-header)
|
||||
#[cfg_attr(feature = "cli", arg(long, help_heading = "Scripting"))]
|
||||
pub print_current: bool,
|
||||
|
||||
/// Set the output format
|
||||
/// If set, overrides all `print_` options
|
||||
/// Will be expanded the same way as preview or commands
|
||||
#[cfg_attr(feature = "cli", arg(long, help_heading = "Scripting"))]
|
||||
pub output_format: Option<String>,
|
||||
|
||||
/// Print the ANSI codes, making the output exactly match the input even when `--ansi` is on
|
||||
#[cfg_attr(feature = "cli", arg(long, help_heading = "Scripting", requires = "ansi"))]
|
||||
pub no_strip_ansi: bool,
|
||||
|
|
@ -906,6 +916,7 @@ impl Default for SkimOptions {
|
|||
listen: None,
|
||||
remote: None,
|
||||
print_header: false,
|
||||
print_current: false,
|
||||
disabled: false,
|
||||
tac: Default::default(),
|
||||
min_query_length: Default::default(),
|
||||
|
|
@ -964,6 +975,7 @@ impl Default for SkimOptions {
|
|||
print_query: Default::default(),
|
||||
print_cmd: Default::default(),
|
||||
print_score: Default::default(),
|
||||
output_format: Default::default(),
|
||||
select_1: Default::default(),
|
||||
exit_0: Default::default(),
|
||||
sync: Default::default(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::SkimItem;
|
||||
use crate::item::MatchedItem;
|
||||
use crate::tui::Event;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -26,6 +27,9 @@ pub struct SkimOutput {
|
|||
/// The selected items.
|
||||
pub selected_items: Vec<Arc<MatchedItem>>,
|
||||
|
||||
/// The current item
|
||||
pub current: Option<Arc<dyn SkimItem>>,
|
||||
|
||||
/// The header
|
||||
pub header: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ where
|
|||
query: self.app.input.to_string(),
|
||||
is_abort,
|
||||
selected_items: self.app.results(),
|
||||
current: self.app.item_list.selected(),
|
||||
header: self.app.header.header.clone(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
use ratatui::text::Line;
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{Debug, Display},
|
||||
};
|
||||
|
||||
use crate::{AsAny, DisplayContext, ItemPreview, PreviewContext};
|
||||
|
||||
|
|
@ -93,3 +96,18 @@ impl<T: AsRef<str> + Send + Sync + 'static> SkimItem for T {
|
|||
Cow::Borrowed(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for dyn SkimItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.text())
|
||||
}
|
||||
}
|
||||
impl Debug for dyn SkimItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"SkimItem {{ text: {}, index: {} }}",
|
||||
self.text(),
|
||||
self.get_index()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
src/tmux.rs
66
src/tmux.rs
|
|
@ -170,6 +170,7 @@ pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
|
|||
// Build args to send to downstream sk invocation
|
||||
let mut tmux_shell_cmd = String::new();
|
||||
let mut prev_is_tmux_flag = false;
|
||||
let mut prev_is_output_format_flag = false;
|
||||
// We keep argv[0] to use in the popup's command
|
||||
for arg in std::env::args() {
|
||||
debug!("Got arg {arg}");
|
||||
|
|
@ -178,6 +179,9 @@ pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
|
|||
if !arg.starts_with("-") {
|
||||
continue;
|
||||
}
|
||||
} else if prev_is_output_format_flag {
|
||||
prev_is_output_format_flag = false;
|
||||
continue;
|
||||
}
|
||||
if arg == "--tmux" {
|
||||
debug!("Found tmux arg, skipping this and the next");
|
||||
|
|
@ -186,9 +190,30 @@ pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
|
|||
} else if arg.starts_with("--tmux") {
|
||||
debug!("Found equal tmux arg, skipping");
|
||||
continue;
|
||||
} else if arg == "--output-format" {
|
||||
debug!("Found output format arg, skipping this and the next");
|
||||
prev_is_output_format_flag = true;
|
||||
continue;
|
||||
} else if arg.starts_with("--output-format") {
|
||||
debug!("Found equal output format arg, skipping");
|
||||
continue;
|
||||
}
|
||||
push_quoted_arg(&mut tmux_shell_cmd, &arg);
|
||||
}
|
||||
// Always add all --print-xxx flags to the child sk command so that the output
|
||||
// is fully structured and can be parsed unconditionally below, regardless of
|
||||
// which flags the user originally passed.
|
||||
for flag in &[
|
||||
"--print-query",
|
||||
"--print-cmd",
|
||||
"--print-header",
|
||||
"--print-current",
|
||||
"--print-score",
|
||||
] {
|
||||
tmux_shell_cmd.push_str(&format!(" {flag}"));
|
||||
}
|
||||
tmux_shell_cmd = tmux_shell_cmd.replace("--output-format", "");
|
||||
|
||||
if has_piped_input {
|
||||
tmux_shell_cmd.push_str(&format!(" <{}", tmp_stdin.display()));
|
||||
}
|
||||
|
|
@ -238,42 +263,48 @@ pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
|
|||
let mut stdout = stdout_bytes.split(output_ending);
|
||||
let _ = std::fs::remove_dir_all(temp_dir);
|
||||
|
||||
let query_str = if opts.print_query && status.success() {
|
||||
stdout.next().expect("Not enough lines to unpack in downstream result")
|
||||
// The child sk process always runs with --print-query, --print-cmd, --print-header,
|
||||
// and --print-score, so we always read those lines unconditionally.
|
||||
let query_str = if status.success() {
|
||||
stdout.next().unwrap_or_default()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let command_str = if opts.print_cmd && status.success() {
|
||||
stdout.next().expect("Not enough lines to unpack in downstream result")
|
||||
let command_str = if status.success() {
|
||||
stdout.next().unwrap_or_default()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let header = if opts.print_header && status.success() {
|
||||
stdout.next().expect("Not enough lines to unpack in downstream result")
|
||||
let header = if status.success() {
|
||||
stdout.next().unwrap_or_default()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let current: Option<Arc<dyn SkimItem>> = if status.success() {
|
||||
let line = stdout.next().unwrap_or_default();
|
||||
if line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Arc::new(SkimTmuxOutput { line: line.to_string() }))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut output_lines: Vec<Arc<MatchedItem>> = vec![];
|
||||
while let Some(line) = stdout.next() {
|
||||
debug!("Adding output line: {line}");
|
||||
let mut item = MatchedItem {
|
||||
// --print-score is always enabled in the child, so every item is followed by its score.
|
||||
let score: i32 = stdout.next().unwrap_or_default().parse().unwrap_or_default();
|
||||
let item = MatchedItem {
|
||||
item: Arc::new(SkimTmuxOutput { line: line.to_string() }),
|
||||
rank: [0; 5],
|
||||
rank: [score, 0, 0, 0, 0],
|
||||
matched_range: None,
|
||||
};
|
||||
if opts.print_score {
|
||||
item.rank = [
|
||||
stdout.next().unwrap_or_default().parse().unwrap_or_default(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
];
|
||||
}
|
||||
output_lines.push(Arc::new(item));
|
||||
}
|
||||
|
||||
|
|
@ -294,6 +325,7 @@ pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
|
|||
query: query_str.to_string(),
|
||||
cmd: command_str.to_string(),
|
||||
selected_items: output_lines,
|
||||
current,
|
||||
header,
|
||||
};
|
||||
Some(skim_output)
|
||||
|
|
|
|||
47
src/util.rs
47
src/util.rs
|
|
@ -168,13 +168,48 @@ pub fn printf(
|
|||
}
|
||||
}
|
||||
s => {
|
||||
if let Some(range) = FieldRange::from_str(s) {
|
||||
let replacement =
|
||||
get_string_by_field(delimiter, &item_text, &range).unwrap_or_default();
|
||||
replaced.push_str(&escape_arg(replacement, true));
|
||||
let (is_plus, stripped) = match s.strip_prefix('+') {
|
||||
Some(x) => (true, x),
|
||||
None => (false, s),
|
||||
};
|
||||
if is_plus {
|
||||
let mut quote_individually = false;
|
||||
|
||||
let (stripped, delim) = stripped.rsplit_once(':').unwrap_or_else(|| {
|
||||
quote_individually = quote_args;
|
||||
(stripped, " ")
|
||||
});
|
||||
if let Some(range) = FieldRange::from_str(stripped) {
|
||||
let expanded = selected
|
||||
.clone()
|
||||
.map(|i| {
|
||||
escape_arg(
|
||||
get_string_by_field(delimiter, &strip_ansi(&i.output()).0, &range)
|
||||
.unwrap_or_default(),
|
||||
quote_individually,
|
||||
)
|
||||
})
|
||||
.reduce(|a: String, b| a.to_owned() + delim + b.as_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
if quote_args && !quote_individually {
|
||||
replaced.push_str(&format!("'{}'", expanded));
|
||||
} else {
|
||||
replaced.push_str(&expanded);
|
||||
}
|
||||
} else {
|
||||
log::warn!("Failed to build multi-item field range from {content}");
|
||||
replaced.push_str(&format!("{{{s}}}"));
|
||||
}
|
||||
} else {
|
||||
log::warn!("Failed to build field range from {content}");
|
||||
replaced.push_str(&format!("{{{s}}}"));
|
||||
if let Some(range) = FieldRange::from_str(stripped) {
|
||||
let replacement =
|
||||
get_string_by_field(delimiter, &item_text, &range).unwrap_or_default();
|
||||
replaced.push_str(&escape_arg(replacement, true));
|
||||
} else {
|
||||
log::warn!("Failed to build field range from {content}");
|
||||
replaced.push_str(&format!("{{{s}}}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,10 +50,45 @@ fn tmux_vanilla() -> Result<()> {
|
|||
let cmd = get_tmux_cmd(&outfile)?;
|
||||
assert!(cmd.starts_with("display-popup"));
|
||||
assert!(cmd.contains("-E"));
|
||||
assert!(cmd.contains("--print-query"));
|
||||
assert!(cmd.contains("--print-cmd"));
|
||||
assert!(cmd.contains("--print-header"));
|
||||
assert!(cmd.contains("--print-current"));
|
||||
assert!(cmd.contains("--print-score"));
|
||||
assert!(!cmd.contains("<"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tmux_output_format() -> Result<()> {
|
||||
let mut tmux = TmuxController::new()?;
|
||||
let outfile = setup_tmux_mock(&tmux)?;
|
||||
tmux.start_sk(
|
||||
None,
|
||||
&[
|
||||
"--tmux",
|
||||
"--output-format",
|
||||
"output-format",
|
||||
"--output-format=output-format",
|
||||
],
|
||||
)?;
|
||||
tmux.until(|_| Path::new(&outfile).exists())?;
|
||||
let cmd = get_tmux_cmd(&outfile)?;
|
||||
assert!(cmd.starts_with("display-popup"));
|
||||
assert!(cmd.contains("-E"));
|
||||
assert!(cmd.contains("--print-query"));
|
||||
assert!(cmd.contains("--print-cmd"));
|
||||
assert!(cmd.contains("--print-header"));
|
||||
assert!(cmd.contains("--print-current"));
|
||||
assert!(cmd.contains("--print-score"));
|
||||
assert!(cmd.contains("--print-score"));
|
||||
assert!(!cmd.contains("output-format"));
|
||||
assert!(!cmd.contains("<"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tmux_stdin() -> Result<()> {
|
||||
let mut tmux = TmuxController::new()?;
|
||||
|
|
@ -95,7 +130,9 @@ fn tmux_quote_zsh() -> Result<()> {
|
|||
println!("{cmd}");
|
||||
assert!(cmd.starts_with("display-popup"));
|
||||
assert!(cmd.contains("-E"));
|
||||
assert!(cmd.contains("sk --bind $'ctrl-a:reload(ls /foo*)' >"));
|
||||
assert!(cmd.contains(
|
||||
"sk --bind $'ctrl-a:reload(ls /foo*)' --print-query --print-cmd --print-header --print-current --print-score >"
|
||||
));
|
||||
assert!(cmd.contains("SKIM_ESCAPED_VAR=;\\;"));
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Reference in a new issue