mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
feat!: internally compute indexes at match time (removes get/set_index) (#1001)
* chore: remove skim::Item run_items wrapper * fix: properly trigger re-render on custom previews * feat: add AppendItems event * feat!: internally compute indexes at match time (removes get/set_index) * chore: generate completions & manpage * chore: better benchmarks --------- Co-authored-by: Skim bot <skim-bot@skim-rs.github.io>
This commit is contained in:
parent
0ac1ac878d
commit
ab514a54c9
|
|
@ -1,206 +1,283 @@
|
|||
use std::fs;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
|
||||
use skim::Typos;
|
||||
use skim::helper::item::DefaultSkimItem;
|
||||
use skim::prelude::*;
|
||||
|
||||
const CHUNK_SIZE: usize = 1024;
|
||||
fn load_lines(file: &str) -> Vec<String> {
|
||||
let data = fs::read_to_string(format!("benches/fixtures/{file}")).expect("{file} missing");
|
||||
data.lines().map(|l| l.to_string()).collect()
|
||||
}
|
||||
|
||||
fn prepare(file: &str, opt_builder: &mut SkimOptionsBuilder) -> (SkimOptions, SkimItemReceiver) {
|
||||
let lines = load_lines(file);
|
||||
let opts = opt_builder.build().unwrap();
|
||||
let (tx, rx) = unbounded();
|
||||
let mut chunk_size = 0;
|
||||
let mut chunk = Vec::new();
|
||||
for line in lines {
|
||||
if chunk_size >= CHUNK_SIZE {
|
||||
tx.send(chunk).unwrap();
|
||||
chunk_size = 0;
|
||||
chunk = Vec::new();
|
||||
}
|
||||
chunk.push(Arc::new(DefaultSkimItem::from(line)) as Arc<dyn SkimItem>);
|
||||
}
|
||||
tx.send(chunk).unwrap();
|
||||
(opts, rx)
|
||||
}
|
||||
|
||||
fn criterion_benchmark_10m(c: &mut Criterion) {
|
||||
c.bench_function("filter_10M_default", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| prepare("10M.txt", SkimOptionsBuilder::default().filter("test")),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_regex", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.regex(true)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| prepare("10M.txt", SkimOptionsBuilder::default().filter("test").regex(true)),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_frizbee", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_frizbee_typos", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.typos(Typos::Smart)
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_clangd", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Clangd)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Clangd),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_fzy", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_fzy_typos", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.typos(Typos::Smart)
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_arinae", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_arinae_typos", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.filter("test")
|
||||
.typos(Typos::Smart)
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn criterion_benchmark_1m(c: &mut Criterion) {
|
||||
c.bench_function("filter_1M_default", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| prepare("1M.txt", SkimOptionsBuilder::default().filter("test")),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("filter_1M_regex", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.regex(true)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| prepare("1M.txt", SkimOptionsBuilder::default().filter("test").regex(true)),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_frizbee", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_frizbee_typos", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.typos(Typos::Smart)
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_clangd", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Clangd)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Clangd),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_fzy", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_fzy_typos", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.typos(Typos::Smart)
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_arinae", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_arinae_typos", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("test")
|
||||
.typos(Typos::Smart)
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("filter_1M_andor", |b| {
|
||||
b.iter(|| {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.cmd("cat benches/fixtures/1M.txt")
|
||||
.filter("boot foo | mnt foo")
|
||||
.build()?;
|
||||
Skim::run_with(opts, None)
|
||||
});
|
||||
b.iter_batched(
|
||||
|| prepare("1M.txt", SkimOptionsBuilder::default().filter("boot foo | mnt foo")),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use skim::fuzzy_matcher::frizbee::FrizbeeMatcher;
|
|||
use skim::prelude::SkimMatcherV2;
|
||||
|
||||
fn load_lines() -> Vec<String> {
|
||||
let data = fs::read_to_string("benches/fixtures/1M.txt").expect("1M.txt missing");
|
||||
let data = fs::read_to_string("benches/fixtures/100K.txt").expect("100K.txt missing");
|
||||
data.lines().map(|l| l.to_string()).collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ fn main() -> color_eyre::Result<()> {
|
|||
let res = Skim::run_items(opts, ["hello", "world"])?;
|
||||
|
||||
for item in res.selected_items {
|
||||
println!("Selected {} (id {})", item.output(), item.get_index());
|
||||
println!("Selected {} (id {})", item.output(), item.rank.index);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ use std::io::Cursor;
|
|||
/// This example demonstrates how to bind custom action callbacks to keyboard shortcuts.
|
||||
///
|
||||
/// It shows how to:
|
||||
/// 1. Create custom action callbacks
|
||||
/// 1. Create custom action callbacks (both sync and async)
|
||||
/// 2. Bind them to specific key combinations
|
||||
/// 3. Use them interactively in skim
|
||||
fn main() {
|
||||
// Create a custom callback that adds a prefix to the query
|
||||
let add_prefix_callback = ActionCallback::new(|app: &mut skim::tui::App| {
|
||||
// Create a synchronous callback that adds a prefix to the query.
|
||||
// Use `new_sync` for plain closures that do not need to await anything.
|
||||
let add_prefix_callback = ActionCallback::new_sync(|app: &mut skim::tui::App| {
|
||||
// Get current query and add prefix
|
||||
let current_query = app.input.value.clone();
|
||||
|
||||
|
|
@ -32,14 +33,17 @@ fn main() {
|
|||
Ok(events)
|
||||
});
|
||||
|
||||
// Create a callback that selects all and exits
|
||||
// Create an async callback that selects all and exits.
|
||||
// Use `new` for async closures or blocks that may await futures.
|
||||
let select_all_callback = ActionCallback::new(|app: &mut skim::tui::App| {
|
||||
let count = app.item_pool.len();
|
||||
|
||||
Ok(vec![
|
||||
Event::Action(Action::SelectAll),
|
||||
Event::Action(Action::Accept(Some(format!("Selected {count} items")))),
|
||||
])
|
||||
async move {
|
||||
// Async work could go here (e.g. HTTP requests, file I/O, …).
|
||||
Ok(vec![
|
||||
Event::Action(Action::SelectAll),
|
||||
Event::Action(Action::Accept(Some(format!("Selected {count} items")))),
|
||||
])
|
||||
}
|
||||
});
|
||||
|
||||
// Build basic options
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ fn main() {
|
|||
let options = SkimOptionsBuilder::default()
|
||||
.height("50%")
|
||||
.multi(true)
|
||||
.preview(String::new()) // preview should be specified to enable preview window
|
||||
.preview("") // preview should be specified to enable preview window
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
env_logger::init();
|
||||
|
||||
let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
|
||||
let _ = tx_item.send(vec![
|
||||
Arc::new(MyItem {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ use skim::prelude::*;
|
|||
#[derive(Debug, Clone)]
|
||||
struct Item {
|
||||
text: String,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl SkimItem for Item {
|
||||
|
|
@ -18,14 +17,6 @@ impl SkimItem for Item {
|
|||
fn preview(&self, _context: PreviewContext) -> ItemPreview {
|
||||
ItemPreview::Text(self.text.to_owned())
|
||||
}
|
||||
|
||||
fn get_index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
fn set_index(&mut self, index: usize) {
|
||||
self.index = index
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
|
|
@ -39,18 +30,9 @@ pub fn main() {
|
|||
let (tx, rx): (SkimItemSender, SkimItemReceiver) = unbounded();
|
||||
|
||||
tx.send(vec![
|
||||
Arc::new(Item {
|
||||
text: "a".into(),
|
||||
index: 0,
|
||||
}) as Arc<dyn SkimItem>,
|
||||
Arc::new(Item {
|
||||
text: "b".into(),
|
||||
index: 1,
|
||||
}) as Arc<dyn SkimItem>,
|
||||
Arc::new(Item {
|
||||
text: "c".into(),
|
||||
index: 2,
|
||||
}) as Arc<dyn SkimItem>,
|
||||
Arc::new(Item { text: "a".into() }) as Arc<dyn SkimItem>,
|
||||
Arc::new(Item { text: "b".into() }) as Arc<dyn SkimItem>,
|
||||
Arc::new(Item { text: "c".into() }) as Arc<dyn SkimItem>,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -60,7 +42,7 @@ pub fn main() {
|
|||
.map(|out| out.selected_items)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|selected_item| (**selected_item).as_any().downcast_ref::<Item>().unwrap().to_owned())
|
||||
.map(|selected_item| selected_item.downcast_item::<Item>().unwrap().to_owned())
|
||||
.collect::<Vec<Item>>();
|
||||
|
||||
for item in selected_items {
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ _sk() {
|
|||
return 0
|
||||
;;
|
||||
--flags)
|
||||
COMPREPLY=($(compgen -W "no-preview-pty show-score" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "no-preview-pty show-score show-index" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--hscroll-off)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ complete -c sk -l tmux -d 'Run in a tmux popup' -r
|
|||
complete -c sk -l log-level -d 'Set the log level' -r
|
||||
complete -c sk -l log-file -d 'Pipe log output to a file' -r
|
||||
complete -c sk -l flags -d 'Feature flags' -r -f -a "no-preview-pty\t'Disable preview PTY on linux'
|
||||
show-score\t'Display the item\'s match score before its value in the item list (for matcher debugging)'"
|
||||
show-score\t'Display the item\'s match score before its value in the item list (for matcher debugging)'
|
||||
show-index\t'Display the item\'s index before its value in the item list'"
|
||||
complete -c sk -l hscroll-off -r
|
||||
complete -c sk -l jump-labels -r
|
||||
complete -c sk -l tail -r
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ module completions {
|
|||
}
|
||||
|
||||
def "nu-complete sk flags" [] {
|
||||
[ "no-preview-pty" "show-score" ]
|
||||
[ "no-preview-pty" "show-score" "show-index" ]
|
||||
}
|
||||
|
||||
# Fuzzy Finder in rust!
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@ zsh\:"Zsh"))' \
|
|||
'--log-level=[Set the log level]:LOG_LEVEL:_default' \
|
||||
'--log-file=[Pipe log output to a file]:LOG_FILE:_default' \
|
||||
'*--flags=[Feature flags]:FLAGS:((no-preview-pty\:"Disable preview PTY on linux"
|
||||
show-score\:"Display the item'\''s match score before its value in the item list (for matcher debugging)"))' \
|
||||
show-score\:"Display the item'\''s match score before its value in the item list (for matcher debugging)"
|
||||
show-index\:"Display the item'\''s index before its value in the item list"))' \
|
||||
'--hscroll-off=[]:HSCROLL_OFF:_default' \
|
||||
'--jump-labels=[]:JUMP_LABELS:_default' \
|
||||
'--tail=[]:TAIL:_default' \
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ fn sk_main(mut opts: SkimOptions) -> Result<i32> {
|
|||
output_format,
|
||||
&bin_options.delimiter,
|
||||
&bin_options.replstr,
|
||||
result.selected_items.iter().map(|x| x.item.clone()),
|
||||
result.selected_items.iter(),
|
||||
result.current,
|
||||
&result.query,
|
||||
&result.cmd,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ impl MatchEngine for MatchAllEngine {
|
|||
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
|
||||
let item_text = item.text();
|
||||
Some(MatchResult {
|
||||
rank: self.rank_builder.build_rank(0, 0, 0, &item_text, item.get_index()),
|
||||
rank: self.rank_builder.build_rank(0, 0, 0, &item_text),
|
||||
matched_range: MatchRange::ByteRange(0, 0),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,9 +101,7 @@ impl MatchEngine for ExactEngine {
|
|||
let (begin, end) = matched_result?;
|
||||
let score = (end - begin) as i32;
|
||||
Some(MatchResult {
|
||||
rank: self
|
||||
.rank_builder
|
||||
.build_rank(score, begin, end, &item_text, item.get_index()),
|
||||
rank: self.rank_builder.build_rank(score, begin, end, &item_text),
|
||||
matched_range: MatchRange::ByteRange(begin, end),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,9 +215,7 @@ impl MatchEngine for FuzzyEngine {
|
|||
|
||||
let (score, begin, end) = best?;
|
||||
Some(MatchResult {
|
||||
rank: self
|
||||
.rank_builder
|
||||
.build_rank(score as i32, begin, end, &item_text, item.get_index()),
|
||||
rank: self.rank_builder.build_rank(score as i32, begin, end, &item_text),
|
||||
matched_range: MatchRange::ByteRange(begin, end),
|
||||
})
|
||||
} else {
|
||||
|
|
@ -254,9 +252,7 @@ impl MatchEngine for FuzzyEngine {
|
|||
let matched_range = MatchRange::Chars(matched_indices);
|
||||
|
||||
Some(MatchResult {
|
||||
rank: self
|
||||
.rank_builder
|
||||
.build_rank(score as i32, begin, end, &item_text, item.get_index()),
|
||||
rank: self.rank_builder.build_rank(score as i32, begin, end, &item_text),
|
||||
matched_range,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,9 +70,7 @@ impl MatchEngine for RegexEngine {
|
|||
let score = (end - begin) as i32;
|
||||
|
||||
Some(MatchResult {
|
||||
rank: self
|
||||
.rank_builder
|
||||
.build_rank(score, begin, end, &item_text, item.get_index()),
|
||||
rank: self.rank_builder.build_rank(score, begin, end, &item_text),
|
||||
matched_range: MatchRange::ByteRange(begin, end),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub(super) const TYPO_BAND_SLACK: usize = 4;
|
|||
/// (standard bonus). Entries that are `0` are not considered separators.
|
||||
pub(super) const SEPARATOR_TABLE: [Score; 128] = {
|
||||
let mut t = [0 as Score; 128];
|
||||
t[b' ' as usize] = 12; // space
|
||||
t[b' ' as usize] = 16; // space
|
||||
t[b'-' as usize] = 10; // hyphen / kebab-case
|
||||
t[b'.' as usize] = 12; // dot (file extensions, domain names)
|
||||
t[b'/' as usize] = 16; // forward slash (path separator — higher bonus)
|
||||
|
|
|
|||
|
|
@ -24,9 +24,6 @@ pub struct DefaultSkimItem {
|
|||
/// The text that will be shown on screen.
|
||||
text: Box<str>,
|
||||
|
||||
/// The index, for use in matching
|
||||
index: usize,
|
||||
|
||||
/// Metadata containing miscellaneous fields when special options are used
|
||||
metadata: Option<Box<DefaultSkimItemMetadata>>,
|
||||
}
|
||||
|
|
@ -60,7 +57,6 @@ impl DefaultSkimItem {
|
|||
trans_fields: &[FieldRange],
|
||||
matching_fields: &[FieldRange],
|
||||
delimiter: &Regex,
|
||||
index: usize,
|
||||
) -> Self {
|
||||
let using_transform_fields = !trans_fields.is_empty();
|
||||
let contains_ansi = Self::contains_ansi_escape(orig_text);
|
||||
|
|
@ -169,7 +165,6 @@ impl DefaultSkimItem {
|
|||
|
||||
DefaultSkimItem {
|
||||
text: temp_text,
|
||||
index,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
|
@ -230,6 +225,15 @@ impl DefaultSkimItem {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<String> for DefaultSkimItem {
|
||||
fn from(value: String) -> Self {
|
||||
Self {
|
||||
text: Box::from(value),
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SkimItem for DefaultSkimItem {
|
||||
#[inline]
|
||||
fn text(&self) -> Cow<'_, str> {
|
||||
|
|
@ -433,14 +437,6 @@ impl SkimItem for DefaultSkimItem {
|
|||
context.to_line(Cow::Borrowed(&self.text))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
fn set_index(&mut self, index: usize) {
|
||||
self.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip ANSI escape sequences from a string
|
||||
|
|
@ -651,7 +647,6 @@ mod test {
|
|||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
|
||||
// text() should return stripped text for matching
|
||||
|
|
@ -693,7 +688,6 @@ mod test {
|
|||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
|
||||
// text() should return "😀text"
|
||||
|
|
@ -727,7 +721,6 @@ mod test {
|
|||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
assert_eq!(
|
||||
item_ansi.text(),
|
||||
|
|
@ -742,7 +735,6 @@ mod test {
|
|||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
assert_eq!(
|
||||
item_no_ansi.text(),
|
||||
|
|
@ -766,7 +758,6 @@ mod test {
|
|||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
|
||||
// Create display context with yellow background highlight for character 0 (the 'g')
|
||||
|
|
@ -805,7 +796,6 @@ mod test {
|
|||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
|
||||
// Create display context with yellow background highlight for characters 1-3 ('re')
|
||||
|
|
@ -846,7 +836,6 @@ mod test {
|
|||
&[],
|
||||
&[],
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
|
||||
// Create display context with yellow background highlight for bytes 1-3 ('re' in stripped text)
|
||||
|
|
@ -886,7 +875,6 @@ mod test {
|
|||
&[],
|
||||
&[], // no matching fields restriction
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
|
||||
// text() should return stripped text "green_text"
|
||||
|
|
@ -918,7 +906,6 @@ mod test {
|
|||
&[], // no transform fields
|
||||
&[FieldRange::Single(2)], // match field 2
|
||||
&delimiter,
|
||||
0,
|
||||
);
|
||||
|
||||
// text() should return text with null bytes stripped for display
|
||||
|
|
|
|||
|
|
@ -203,7 +203,6 @@ impl SkimItemReader {
|
|||
matching_fields: Vec<FieldRange>,
|
||||
) {
|
||||
let mut buffer = Vec::with_capacity(option.buf_size);
|
||||
let mut line_idx = 0;
|
||||
let mut items_to_send = Vec::with_capacity(ITEMS_BUFFER_SIZE);
|
||||
let mut last_send_time = Instant::now();
|
||||
let send_timeout = Duration::from_millis(SEND_TIMEOUT_MS);
|
||||
|
|
@ -227,7 +226,7 @@ impl SkimItemReader {
|
|||
continue;
|
||||
};
|
||||
|
||||
trace!("got item {} with index {}", line, line_idx);
|
||||
trace!("got item {}", line);
|
||||
|
||||
let raw_item = DefaultSkimItem::new(
|
||||
line,
|
||||
|
|
@ -235,11 +234,8 @@ impl SkimItemReader {
|
|||
&transform_fields,
|
||||
&matching_fields,
|
||||
&option.delimiter,
|
||||
line_idx,
|
||||
);
|
||||
items_to_send.push(Arc::new(raw_item) as Arc<dyn SkimItem>);
|
||||
|
||||
line_idx += 1;
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("Got {err:?} when reading, skipping");
|
||||
|
|
@ -332,7 +328,6 @@ impl SkimItemReader {
|
|||
&[],
|
||||
&[],
|
||||
&Regex::new(DELIMITER_STR).unwrap(),
|
||||
0,
|
||||
)) as Arc<dyn SkimItem>
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
16
src/item.rs
16
src/item.rs
|
|
@ -60,13 +60,14 @@ impl RankBuilder {
|
|||
///
|
||||
/// The values are stored as-is; the tiebreak ordering and sign-flipping are
|
||||
/// applied lazily by [`Rank::sort_key`] at comparison time.
|
||||
pub fn build_rank(&self, score: i32, begin: usize, end: usize, item_text: &str, index: usize) -> Rank {
|
||||
/// The `index` will be overriden later
|
||||
pub fn build_rank(&self, score: i32, begin: usize, end: usize, item_text: &str) -> Rank {
|
||||
Rank {
|
||||
score,
|
||||
begin: begin as i32,
|
||||
end: end as i32,
|
||||
length: item_text.len() as i32,
|
||||
index: index as i32,
|
||||
index: Default::default(),
|
||||
path_name_offset: Self::path_name_offset(item_text),
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +132,7 @@ impl std::fmt::Debug for MatchedItem {
|
|||
|
||||
impl Hash for MatchedItem {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
state.write_usize(self.get_index());
|
||||
state.write_i32(self.rank.index);
|
||||
self.text().hash(state);
|
||||
}
|
||||
}
|
||||
|
|
@ -244,11 +245,18 @@ impl MatchedItem {
|
|||
}
|
||||
}
|
||||
|
||||
impl MatchedItem {
|
||||
/// Downcast the MatchedItem to the corresponding SkimItem struct
|
||||
pub fn downcast_item<T: SkimItem>(&self) -> Option<&T> {
|
||||
(*self.item).as_any().downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
use std::cmp::Ordering as CmpOrd;
|
||||
|
||||
impl PartialEq for MatchedItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.text().eq(&other.text()) && self.get_index().eq(&other.get_index())
|
||||
self.text().eq(&other.text()) && self.rank.index.eq(&other.rank.index)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ impl Matcher {
|
|||
// if we took items inside the spawned closure, a subsequent restart_matcher()
|
||||
// could call kill() + reset() before the old closure runs, causing the old
|
||||
// closure to re-take items that should belong to the new matcher.
|
||||
let start = item_pool.num_taken();
|
||||
let items = item_pool.take();
|
||||
let total = items.len();
|
||||
trace!("matcher start, total: {}", total);
|
||||
|
|
@ -212,9 +213,10 @@ impl Matcher {
|
|||
let matched_items: Vec<MatchedItem> = items
|
||||
.into_par_iter()
|
||||
.with_min_len(CHUNK_SIZE)
|
||||
.enumerate()
|
||||
.fold(
|
||||
|| (Vec::new(), 0usize, 0usize), // (local_matches, local_processed, local_matched)
|
||||
|(mut local_matches, mut local_processed, mut local_matched), item| {
|
||||
|(mut local_matches, mut local_processed, mut local_matched), (index, item)| {
|
||||
// Check interrupt once at the start of each chunk boundary.
|
||||
// The fold processes items sequentially within each rayon work unit,
|
||||
// so checking every CHUNK_SIZE items amortizes the atomic load.
|
||||
|
|
@ -226,9 +228,11 @@ impl Matcher {
|
|||
|
||||
if let Some(match_result) = matcher_engine.match_item(item.as_ref()) {
|
||||
local_matched += 1;
|
||||
let mut rank = match_result.rank;
|
||||
rank.index = (index + start) as i32;
|
||||
local_matches.push(MatchedItem {
|
||||
item,
|
||||
rank: match_result.rank,
|
||||
rank,
|
||||
rank_builder: rank_builder.clone(),
|
||||
matched_range: Some(match_result.matched_range),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1275,6 +1275,8 @@ pub enum FeatureFlag {
|
|||
NoPreviewPty,
|
||||
/// Display the item's match score before its value in the item list (for matcher debugging)
|
||||
ShowScore,
|
||||
/// Display the item's index before its value in the item list
|
||||
ShowIndex,
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use crate::SkimItem;
|
||||
use crate::item::MatchedItem;
|
||||
use crate::tui::Event;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Output from running skim, containing the final selection and state
|
||||
#[derive(Debug)]
|
||||
|
|
@ -25,10 +23,10 @@ pub struct SkimOutput {
|
|||
pub cmd: String,
|
||||
|
||||
/// The selected items.
|
||||
pub selected_items: Vec<Arc<MatchedItem>>,
|
||||
pub selected_items: Vec<MatchedItem>,
|
||||
|
||||
/// The current item
|
||||
pub current: Option<Arc<dyn SkimItem>>,
|
||||
pub current: Option<MatchedItem>,
|
||||
|
||||
/// The header
|
||||
pub header: String,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ pub use crate::helper::selector::DefaultSkimSelector;
|
|||
pub use crate::options::{SkimOptions, SkimOptionsBuilder};
|
||||
pub use crate::output::SkimOutput;
|
||||
pub use crate::reader::CommandCollector;
|
||||
pub use crate::skim_item::Item;
|
||||
pub use crate::tui::{Event, PreviewCallback, event::Action};
|
||||
pub use crate::*;
|
||||
pub use kanal::{Receiver, Sender, bounded, unbounded};
|
||||
|
|
|
|||
|
|
@ -95,13 +95,11 @@ impl Skim {
|
|||
const BATCH_SIZE: usize = 1024;
|
||||
let (tx, rx) = crate::prelude::unbounded();
|
||||
let mut batch: Vec<Arc<dyn SkimItem>> = Vec::with_capacity(BATCH_SIZE);
|
||||
for (idx, raw_item) in items.into_iter().enumerate() {
|
||||
for item in items {
|
||||
if batch.len() == 1024 {
|
||||
tx.send(batch)?;
|
||||
batch = Vec::with_capacity(BATCH_SIZE);
|
||||
}
|
||||
let mut item = crate::prelude::Item::from(raw_item);
|
||||
item.set_index(idx);
|
||||
batch.push(Arc::new(item) as Arc<dyn SkimItem>);
|
||||
}
|
||||
tx.send(batch)?;
|
||||
|
|
|
|||
|
|
@ -75,17 +75,6 @@ pub trait SkimItem: AsAny + Send + Sync + 'static {
|
|||
fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get index, for matching purposes
|
||||
///
|
||||
/// Implemented as no-op for retro-compatibility purposes
|
||||
fn get_index(&self) -> usize {
|
||||
0
|
||||
}
|
||||
/// Set index, for matching purposes
|
||||
///
|
||||
/// Implemented as no-op for retro-compatibility purposes
|
||||
fn set_index(&mut self, _index: usize) {}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
|
@ -97,47 +86,6 @@ impl<T: AsRef<str> + Send + Sync + 'static> SkimItem for T {
|
|||
}
|
||||
}
|
||||
|
||||
/// A basic SkimItem implementation for basic types
|
||||
pub struct Item<T: SkimItem> {
|
||||
inner: T,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<T: SkimItem> SkimItem for Item<T> {
|
||||
fn text(&self) -> Cow<'_, str> {
|
||||
self.inner.text()
|
||||
}
|
||||
fn display<'a>(&'a self, context: DisplayContext) -> Line<'a> {
|
||||
self.inner.display(context)
|
||||
}
|
||||
|
||||
fn preview(&self, context: PreviewContext) -> ItemPreview {
|
||||
self.inner.preview(context)
|
||||
}
|
||||
|
||||
fn output(&self) -> Cow<'_, str> {
|
||||
self.inner.output()
|
||||
}
|
||||
|
||||
fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> {
|
||||
self.inner.get_matching_ranges()
|
||||
}
|
||||
|
||||
fn get_index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
fn set_index(&mut self, index: usize) {
|
||||
self.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SkimItem> From<T> for Item<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self { inner: value, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for dyn SkimItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.text())
|
||||
|
|
@ -145,10 +93,6 @@ impl Display for dyn SkimItem {
|
|||
}
|
||||
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()
|
||||
))
|
||||
f.write_fmt(format_args!("SkimItem {{ text: {} }}", self.text(),))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
src/tmux.rs
13
src/tmux.rs
|
|
@ -284,18 +284,23 @@ pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
|
|||
}
|
||||
.to_string();
|
||||
|
||||
let current: Option<Arc<dyn SkimItem>> = if status.success() {
|
||||
let current: Option<MatchedItem> = if status.success() {
|
||||
let line = stdout.next().unwrap_or_default();
|
||||
if line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Arc::new(SkimTmuxOutput { line: line.to_string() }))
|
||||
Some(MatchedItem {
|
||||
item: Arc::new(SkimTmuxOutput { line: line.to_string() }),
|
||||
rank: Rank::default(),
|
||||
rank_builder: Arc::new(RankBuilder::default()),
|
||||
matched_range: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut output_lines: Vec<Arc<MatchedItem>> = vec![];
|
||||
let mut output_lines: Vec<MatchedItem> = vec![];
|
||||
while let Some(line) = stdout.next() {
|
||||
debug!("Adding output line: {line}");
|
||||
// --print-score is always enabled in the child, so every item is followed by its score.
|
||||
|
|
@ -309,7 +314,7 @@ pub fn run_with(opts: &SkimOptions) -> Option<SkimOutput> {
|
|||
rank_builder: Arc::new(RankBuilder::default()),
|
||||
matched_range: None,
|
||||
};
|
||||
output_lines.push(Arc::new(item));
|
||||
output_lines.push(item);
|
||||
}
|
||||
|
||||
let is_abort = !status.success();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ use crate::tui::layout::{AppLayout, LayoutTemplate};
|
|||
use crate::tui::options::TuiLayout;
|
||||
use crate::tui::statusline::InfoDisplay;
|
||||
use crate::tui::widget::SkimWidget;
|
||||
use crate::util;
|
||||
use crate::{ItemPreview, PreviewContext, SkimItem, SkimOptions};
|
||||
use crate::{Rank, util};
|
||||
|
||||
use super::Event;
|
||||
use super::Tui;
|
||||
|
|
@ -404,6 +404,7 @@ impl App {
|
|||
{
|
||||
let selection: Vec<_> = self.item_list.selection.iter().map(|i| i.text().into_owned()).collect();
|
||||
let selection_str: Vec<_> = selection.iter().map(|s| s.as_str()).collect();
|
||||
let selected = self.item_list.selected();
|
||||
let ctx = PreviewContext {
|
||||
query: &self.input.value,
|
||||
cmd_query: if self.options.interactive {
|
||||
|
|
@ -413,21 +414,21 @@ impl App {
|
|||
},
|
||||
width: self.preview.cols as usize,
|
||||
height: self.preview.rows as usize,
|
||||
current_index: self.item_list.selected().map(|i| i.get_index()).unwrap_or_default(),
|
||||
current_selection: &self
|
||||
.item_list
|
||||
.selected()
|
||||
.map(|i| i.text().into_owned())
|
||||
.unwrap_or_default(),
|
||||
current_index: selected.as_ref().map(|i| i.rank.index as usize).unwrap_or_default(),
|
||||
current_selection: &selected.map(|i| i.text().into_owned()).unwrap_or_default(),
|
||||
selected_indices: &self
|
||||
.item_list
|
||||
.selection
|
||||
.iter()
|
||||
.map(|v| v.get_index())
|
||||
.map(|v| v.rank.index as usize)
|
||||
.collect::<Vec<_>>(),
|
||||
selections: &selection_str,
|
||||
};
|
||||
let preview = item.preview(ctx);
|
||||
let preview_ready = !matches!(
|
||||
preview,
|
||||
ItemPreview::Global | ItemPreview::Command(_) | ItemPreview::CommandWithPos(_, _)
|
||||
);
|
||||
match preview {
|
||||
ItemPreview::Command(cmd) => self.preview.spawn(tui, &self.expand_cmd(&cmd, true))?,
|
||||
ItemPreview::Text(t) | ItemPreview::AnsiText(t) => self.preview.content(t.bytes().collect())?,
|
||||
|
|
@ -464,12 +465,15 @@ impl App {
|
|||
.content_with_position(t.bytes().collect(), preview_position)?,
|
||||
ItemPreview::Global => self.preview.spawn(tui, &self.expand_cmd(preview_opt, true))?,
|
||||
}
|
||||
if preview_ready {
|
||||
let _ = tui.event_tx.try_send(Event::PreviewReady);
|
||||
}
|
||||
} else if let Some(cb) = &self.options.preview_fn {
|
||||
let selection: Vec<Arc<dyn SkimItem>>;
|
||||
if self.options.multi {
|
||||
selection = self.item_list.selection.iter().map(|i| i.item.clone()).collect();
|
||||
} else if let Some(sel) = self.item_list.selected() {
|
||||
selection = vec![sel];
|
||||
selection = vec![sel.item];
|
||||
} else {
|
||||
selection = Vec::new();
|
||||
}
|
||||
|
|
@ -537,7 +541,7 @@ impl App {
|
|||
let offset = self.calculate_preview_offset(offset_expr);
|
||||
self.preview.set_offset(offset);
|
||||
}
|
||||
tui.event_tx.try_send(Event::Render)?;
|
||||
self.needs_render();
|
||||
}
|
||||
Event::Error(msg) => {
|
||||
tui.exit()?;
|
||||
|
|
@ -587,6 +591,10 @@ impl App {
|
|||
self.item_pool.clear();
|
||||
self.restart_matcher(true);
|
||||
}
|
||||
Event::AppendItems(items) => {
|
||||
self.item_pool.append(items.to_owned());
|
||||
self.restart_matcher(false);
|
||||
}
|
||||
Event::Reload(_) => {
|
||||
unreachable!("Reload is handled by the TUI event loop in lib.rs")
|
||||
}
|
||||
|
|
@ -644,10 +652,14 @@ impl App {
|
|||
AppendAndSelect => {
|
||||
let value = self.input.value.clone();
|
||||
let item: Arc<dyn SkimItem> = Arc::new(value);
|
||||
let rank = Rank {
|
||||
index: self.item_pool.len() as i32,
|
||||
..Default::default()
|
||||
};
|
||||
self.item_pool.append(vec![item.clone()]);
|
||||
self.item_list.append(&mut vec![MatchedItem {
|
||||
item,
|
||||
rank: Default::default(),
|
||||
rank,
|
||||
rank_builder: self.matcher.rank_builder.clone(),
|
||||
matched_range: None,
|
||||
}]);
|
||||
|
|
@ -1101,18 +1113,14 @@ impl App {
|
|||
}
|
||||
|
||||
/// Returns the selected items as results
|
||||
pub fn results(&mut self) -> Vec<Arc<MatchedItem>> {
|
||||
pub fn results(&mut self) -> Vec<MatchedItem> {
|
||||
if self.options.filter.is_some() {
|
||||
// In filter mode, drain items to avoid cloning
|
||||
self.item_list.items.drain(..).map(Arc::new).collect()
|
||||
self.item_list.items.drain(..).collect()
|
||||
} else if self.options.multi && !self.item_list.selection.is_empty() {
|
||||
self.item_list
|
||||
.selection
|
||||
.iter()
|
||||
.map(|item| Arc::new(item.clone()))
|
||||
.collect()
|
||||
} else if let Some(sel) = self.item_list.items.get(self.item_list.current) {
|
||||
vec![Arc::new(sel.clone())]
|
||||
self.item_list.selection.clone().into_iter().collect()
|
||||
} else if let Some(sel) = self.item_list.selected() {
|
||||
vec![sel]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
|
|
@ -1224,7 +1232,7 @@ impl App {
|
|||
cmd,
|
||||
&self.options.delimiter,
|
||||
&self.options.replstr,
|
||||
self.item_list.selection.iter().map(|x| x.item.clone()),
|
||||
self.item_list.selection.iter(),
|
||||
self.item_list.selected(),
|
||||
&self.input.value,
|
||||
&self.input.value,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,57 @@
|
|||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::exhaustive_match;
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use derive_more::{Debug, Eq, PartialEq};
|
||||
|
||||
type ActionCallbackFn =
|
||||
dyn Fn(&mut crate::tui::App) -> Result<Vec<Event>, Box<dyn std::error::Error + Sync + Send>> + Send;
|
||||
type BoxError = Box<dyn std::error::Error + Sync + Send>;
|
||||
type BoxFuture<'a> = Pin<Box<dyn Future<Output = Result<Vec<Event>, BoxError>> + Send + 'a>>;
|
||||
|
||||
/// Trait object stored inside [`ActionCallback`].
|
||||
///
|
||||
/// Having an explicit trait (rather than a bare `dyn Fn` type alias) allows
|
||||
/// Rust to correctly resolve the higher-ranked lifetime in the return type.
|
||||
trait AsyncCallbackFn: Send {
|
||||
fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a>;
|
||||
}
|
||||
|
||||
/// Adapter that stores a concrete async closure and implements [`AsyncCallbackFn`].
|
||||
struct AsyncFnWrapper<F>(F);
|
||||
|
||||
impl<F, Fut> AsyncCallbackFn for AsyncFnWrapper<F>
|
||||
where
|
||||
F: for<'a> Fn(&'a mut crate::tui::App) -> Fut + Send,
|
||||
Fut: Future<Output = Result<Vec<Event>, BoxError>> + Send + 'static,
|
||||
{
|
||||
fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a> {
|
||||
Box::pin((self.0)(app))
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter that stores a plain synchronous closure and implements [`AsyncCallbackFn`].
|
||||
struct SyncFnWrapper<F>(F);
|
||||
|
||||
impl<F> AsyncCallbackFn for SyncFnWrapper<F>
|
||||
where
|
||||
F: Fn(&mut crate::tui::App) -> Result<Vec<Event>, BoxError> + Send,
|
||||
{
|
||||
fn call<'a>(&'a self, app: &'a mut crate::tui::App) -> BoxFuture<'a> {
|
||||
Box::pin(std::future::ready((self.0)(app)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom action callback that receives a mutable reference to the App.
|
||||
///
|
||||
/// The closure will be called with a mutable reference to App and should return
|
||||
/// a vec of events that will be processed after the callback completes.
|
||||
///
|
||||
/// Both sync and async closures are supported:
|
||||
/// - Use [`ActionCallback::new`] to wrap an **async** closure or block.
|
||||
/// - Use [`ActionCallback::new_sync`] to wrap a plain synchronous closure.
|
||||
#[derive(Clone)]
|
||||
pub struct ActionCallback(Arc<Mutex<ActionCallbackFn>>);
|
||||
pub struct ActionCallback(Arc<Mutex<dyn AsyncCallbackFn>>);
|
||||
|
||||
impl std::fmt::Debug for ActionCallback {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
|
@ -21,24 +60,50 @@ impl std::fmt::Debug for ActionCallback {
|
|||
}
|
||||
|
||||
impl ActionCallback {
|
||||
/// Create a new action callback from a closure.
|
||||
/// Create a new action callback from an **async** closure or block.
|
||||
///
|
||||
/// The closure will be called with a mutable reference to App and should return a vec of
|
||||
/// events that will be run after the callback is done.
|
||||
pub fn new<F>(f: F) -> Self
|
||||
/// ```rust,ignore
|
||||
/// ActionCallback::new(|app| async move {
|
||||
/// // async work here …
|
||||
/// Ok(vec![])
|
||||
/// });
|
||||
/// ```
|
||||
pub fn new<F, Fut>(f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut crate::tui::App) -> Result<Vec<Event>, Box<dyn std::error::Error + Sync + Send>> + Send + 'static,
|
||||
F: for<'a> Fn(&'a mut crate::tui::App) -> Fut + Send + 'static,
|
||||
Fut: Future<Output = Result<Vec<Event>, BoxError>> + Send + 'static,
|
||||
{
|
||||
Self(Arc::new(Mutex::new(f)))
|
||||
Self(Arc::new(Mutex::new(AsyncFnWrapper(f))))
|
||||
}
|
||||
|
||||
/// Call the callback with an App reference.
|
||||
pub(crate) fn call(
|
||||
&self,
|
||||
app: &mut crate::tui::App,
|
||||
) -> Result<Vec<Event>, Box<dyn std::error::Error + Sync + Send>> {
|
||||
/// Create a new action callback from a plain **synchronous** closure.
|
||||
///
|
||||
/// This is a convenience wrapper; the closure is lifted into an immediately-
|
||||
/// resolving future so it integrates with the same async call site.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ActionCallback::new_sync(|app| {
|
||||
/// Ok(vec![Event::Action(Action::SelectAll)])
|
||||
/// });
|
||||
/// ```
|
||||
pub fn new_sync<F>(f: F) -> Self
|
||||
where
|
||||
F: Fn(&mut crate::tui::App) -> Result<Vec<Event>, BoxError> + Send + 'static,
|
||||
{
|
||||
Self(Arc::new(Mutex::new(SyncFnWrapper(f))))
|
||||
}
|
||||
|
||||
/// Call the callback with an App reference, driving the returned future to completion.
|
||||
///
|
||||
/// Must be called from within a Tokio multi-thread runtime context.
|
||||
pub(crate) fn call(&self, app: &mut crate::tui::App) -> Result<Vec<Event>, BoxError> {
|
||||
let callback = self.0.lock().unwrap();
|
||||
callback(app)
|
||||
let fut = callback.call(app);
|
||||
// We are inside a synchronous call stack that originates from an async
|
||||
// tokio context. `block_in_place` moves the current thread out of the
|
||||
// async worker pool temporarily so we can block on the future without
|
||||
// starving the runtime.
|
||||
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +132,8 @@ pub enum Event {
|
|||
InvalidInput,
|
||||
/// An action was triggered
|
||||
Action(Action),
|
||||
/// Append items to the pool
|
||||
AppendItems(Vec<Arc<dyn crate::SkimItem>>),
|
||||
/// Clear all items
|
||||
ClearItems,
|
||||
/// Clear the screen
|
||||
|
|
@ -349,7 +416,7 @@ pub fn parse_action(raw_action: &str) -> Option<Action> {
|
|||
"unreachable-if-non-matched" => Some(IfNonMatched(Default::default(), None)),
|
||||
"unreachable-if-query-empty" => Some(IfQueryEmpty(Default::default(), None)),
|
||||
"unreachable-if-query-not-empty" => Some(IfQueryNotEmpty(Default::default(), None)),
|
||||
"custom-do-not-use-from-cli" => Some(Custom(ActionCallback::new(|_: &mut crate::tui::App| { Ok(Vec::new()) }))),
|
||||
"custom-do-not-use-from-cli" => Some(Custom(ActionCallback::new_sync(|_: &mut crate::tui::App| { Ok(Vec::new()) }))),
|
||||
}
|
||||
default _ => None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ use ratatui::widgets::{Block, Borders, Clear, List, ListDirection, ListItem, Lis
|
|||
use regex::Regex;
|
||||
use unicode_display_width::width as display_width;
|
||||
|
||||
use crate::options::feature_flag;
|
||||
use crate::tui::util::char_display_width;
|
||||
use crate::{
|
||||
DisplayContext, MatchRange, Selector, SkimItem, SkimOptions,
|
||||
DisplayContext, MatchRange, Selector, SkimOptions,
|
||||
item::MatchedItem,
|
||||
spinlock::SpinLock,
|
||||
theme::ColorTheme,
|
||||
|
|
@ -75,7 +76,8 @@ pub struct ItemList {
|
|||
/// Border type, if borders are enabled
|
||||
pub border: Option<BorderType>,
|
||||
/// When true, prepend each item's match score to its display text
|
||||
print_score: bool,
|
||||
show_score: bool,
|
||||
show_index: bool,
|
||||
}
|
||||
|
||||
impl Default for ItemList {
|
||||
|
|
@ -109,7 +111,8 @@ impl Default for ItemList {
|
|||
cycle: false,
|
||||
wrap: false,
|
||||
border: None,
|
||||
print_score: false,
|
||||
show_score: false,
|
||||
show_index: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,8 +130,8 @@ impl ItemList {
|
|||
}
|
||||
|
||||
/// Returns the currently selected item, if any
|
||||
pub fn selected(&self) -> Option<Arc<dyn SkimItem>> {
|
||||
self.items.get(self.cursor()).map(|x| x.item.clone())
|
||||
pub fn selected(&self) -> Option<MatchedItem> {
|
||||
self.items.get(self.cursor()).cloned()
|
||||
}
|
||||
|
||||
/// Appends new matched items to the list
|
||||
|
|
@ -571,7 +574,8 @@ impl SkimWidget for ItemList {
|
|||
cycle: options.cycle,
|
||||
wrap: options.wrap_items,
|
||||
border: options.border,
|
||||
print_score: options.flags.contains(&crate::options::FeatureFlag::ShowScore),
|
||||
show_score: feature_flag!(options, ShowScore),
|
||||
show_index: feature_flag!(options, ShowIndex),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -741,14 +745,21 @@ impl SkimWidget for ItemList {
|
|||
},
|
||||
theme.selected,
|
||||
));
|
||||
// Optionally prepend the match score for debugging
|
||||
if this.print_score {
|
||||
// Optionally prepend debug fields
|
||||
if this.show_score {
|
||||
let score = item.rank.score;
|
||||
spans.push(Span::styled(
|
||||
format!("[{score}] "),
|
||||
if is_current { theme.current } else { theme.normal },
|
||||
));
|
||||
}
|
||||
if this.show_index {
|
||||
let index = item.rank.index;
|
||||
spans.push(Span::styled(
|
||||
format!("[{index}] "),
|
||||
if is_current { theme.current } else { theme.normal },
|
||||
));
|
||||
}
|
||||
spans.extend(display_line.spans);
|
||||
|
||||
if *wrap {
|
||||
|
|
|
|||
64
src/util.rs
64
src/util.rs
|
|
@ -1,12 +1,11 @@
|
|||
use crate::SkimItem;
|
||||
use crate::field::FieldRange;
|
||||
use crate::field::get_string_by_field;
|
||||
use crate::helper::item::strip_ansi;
|
||||
use crate::item::MatchedItem;
|
||||
use regex::Regex;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::prelude::v1::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
/// Unescape a delimiter string to handle escape sequences like \x00, \t, \n, etc.
|
||||
|
|
@ -90,12 +89,12 @@ pub fn read_file_lines(filename: &str) -> std::result::Result<Vec<String>, std::
|
|||
/// - `{cq}` -> current command query
|
||||
///
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn printf(
|
||||
pub fn printf<'a>(
|
||||
pattern: &str,
|
||||
delimiter: &Regex,
|
||||
replstr: &str,
|
||||
selected: impl Iterator<Item = Arc<dyn SkimItem>> + std::clone::Clone,
|
||||
current: Option<Arc<dyn SkimItem>>,
|
||||
selected: impl Iterator<Item = &'a MatchedItem> + std::clone::Clone,
|
||||
current: Option<MatchedItem>,
|
||||
query: &str,
|
||||
command_query: &str,
|
||||
quote_args: bool,
|
||||
|
|
@ -133,14 +132,14 @@ pub fn printf(
|
|||
"q" => replaced.push_str(&escaped_query),
|
||||
"cq" => replaced.push_str(&escaped_cmd_query),
|
||||
"n" if current.as_ref().is_some() => {
|
||||
replaced.push_str(current.as_ref().unwrap().get_index().to_string().as_str());
|
||||
replaced.push_str(current.as_ref().unwrap().rank.index.to_string().as_str());
|
||||
}
|
||||
s if s == "+n" || s.starts_with("+n:") || s == "+" || s.starts_with("+:") => {
|
||||
let is_n = s.starts_with("+n");
|
||||
let accessor = if is_n {
|
||||
|i: &Arc<dyn SkimItem>| i.get_index().to_string()
|
||||
|i: &MatchedItem| i.rank.index.to_string()
|
||||
} else {
|
||||
|i: &Arc<dyn SkimItem>| strip_ansi(&i.output()).0
|
||||
|i: &MatchedItem| strip_ansi(&i.output()).0
|
||||
};
|
||||
let mut quote_individually = false;
|
||||
|
||||
|
|
@ -151,7 +150,7 @@ pub fn printf(
|
|||
|
||||
let mut expanded = selected
|
||||
.clone()
|
||||
.map(|i| escape_arg(&accessor(&i), quote_individually))
|
||||
.map(|i| escape_arg(&accessor(i), quote_individually))
|
||||
.reduce(|a: String, b| a.to_owned() + delim + b.as_str())
|
||||
.unwrap_or_default();
|
||||
if expanded.is_empty() {
|
||||
|
|
@ -238,8 +237,19 @@ pub fn printf(
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::SkimItem;
|
||||
use crate::Rank;
|
||||
use crate::item::{MatchedItem, RankBuilder};
|
||||
use regex::Regex;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn make_item(s: &'static str) -> MatchedItem {
|
||||
MatchedItem {
|
||||
item: Arc::new(s),
|
||||
rank: Rank::default(),
|
||||
rank_builder: Arc::new(RankBuilder::default()),
|
||||
matched_range: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unescape_delimiter() {
|
||||
|
|
@ -277,11 +287,11 @@ mod test {
|
|||
#[test]
|
||||
fn test_printf() {
|
||||
let pattern = "[1] {} [2] {..2} [3] {2..} [4] {+} [5] {q} [6] {cq} [7] {+:, } [8] {+n:','}";
|
||||
let items: Vec<Arc<dyn SkimItem>> = vec![
|
||||
Arc::new("item 1"),
|
||||
Arc::new("item 2"),
|
||||
Arc::new("item 3"),
|
||||
Arc::new("item 4"),
|
||||
let items = [
|
||||
make_item("item 1"),
|
||||
make_item("item 2"),
|
||||
make_item("item 3"),
|
||||
make_item("item 4"),
|
||||
];
|
||||
let delimiter = Regex::new(" ").unwrap();
|
||||
assert_eq!(
|
||||
|
|
@ -289,8 +299,8 @@ mod test {
|
|||
pattern,
|
||||
&delimiter,
|
||||
"{}",
|
||||
items.iter().cloned(),
|
||||
Some(Arc::new("item 2")),
|
||||
items.iter(),
|
||||
Some(make_item("item 2")),
|
||||
"query",
|
||||
"cmd query",
|
||||
true
|
||||
|
|
@ -305,10 +315,8 @@ mod test {
|
|||
"{+}",
|
||||
&Regex::new(" ").unwrap(),
|
||||
"{}",
|
||||
[Arc::new("1"), Arc::new("2")]
|
||||
.iter()
|
||||
.map(|x| x.clone() as Arc<dyn SkimItem>),
|
||||
Some(Arc::new("1")),
|
||||
[make_item("1"), make_item("2")].iter(),
|
||||
Some(make_item("1")),
|
||||
"q",
|
||||
"cq",
|
||||
true
|
||||
|
|
@ -320,8 +328,8 @@ mod test {
|
|||
"{+}",
|
||||
&Regex::new(" ").unwrap(),
|
||||
"{}",
|
||||
vec![].into_iter(),
|
||||
Some(Arc::new("1")),
|
||||
[].iter(),
|
||||
Some(make_item("1")),
|
||||
"q",
|
||||
"cq",
|
||||
true
|
||||
|
|
@ -336,8 +344,8 @@ mod test {
|
|||
"{}",
|
||||
&Regex::new(" ").unwrap(),
|
||||
"{}",
|
||||
vec![].into_iter(),
|
||||
Some(Arc::new("{..2}")),
|
||||
[].iter(),
|
||||
Some(make_item("{..2}")),
|
||||
"q",
|
||||
"cq",
|
||||
true
|
||||
|
|
@ -352,10 +360,8 @@ mod test {
|
|||
"{} ##",
|
||||
&Regex::new(" ").unwrap(),
|
||||
"##",
|
||||
[Arc::new("1"), Arc::new("2")]
|
||||
.iter()
|
||||
.map(|x| x.clone() as Arc<dyn SkimItem>),
|
||||
Some(Arc::new("1")),
|
||||
[make_item("1"), make_item("2")].iter(),
|
||||
Some(make_item("1")),
|
||||
"q",
|
||||
"cq",
|
||||
true
|
||||
|
|
|
|||
|
|
@ -176,6 +176,13 @@ impl TestHarness {
|
|||
self.tick()?;
|
||||
|
||||
self.handle_remaining_events()?;
|
||||
|
||||
// Force a final render so that any state changes (e.g. PreviewReady) that
|
||||
// were processed inside handle_remaining_events are reflected in the buffer
|
||||
// before we take the snapshot. We bypass the frame-rate throttle by sending
|
||||
// Render directly instead of relying on the Heartbeat path.
|
||||
self.send(Event::Render)?;
|
||||
self.tick()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -261,32 +268,43 @@ impl TestHarness {
|
|||
// Process any queued events first (including RunPreview)
|
||||
self.tick()?;
|
||||
|
||||
// Now check if there's a pending preview task
|
||||
// If not, there's nothing to wait for
|
||||
if let Some(ref handle) = self.skim.app().preview.thread_handle {
|
||||
if handle.is_finished() {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
// If there's no preview task running, nothing to wait for
|
||||
let has_pending = match self.skim.app().preview.thread_handle {
|
||||
Some(ref handle) => !handle.is_finished(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
if !has_pending {
|
||||
// Thread is already done (or was never started). Drain any events it may
|
||||
// have sent (e.g. PreviewReady) that arrived after our initial tick().
|
||||
self.tick()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Wait for preview to execute
|
||||
// With multi-threaded runtime, spawned tasks run on background threads
|
||||
// Wait for the preview thread to finish, then drain its events.
|
||||
let timeout = std::time::Duration::from_secs(2);
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
// Sleep to give background tasks time to execute
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
// Sleep to give the background thread time to make progress
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
// Drain events then process, so we can check for PreviewReady
|
||||
let mut events = Vec::new();
|
||||
while let Ok(event) = self.skim.tui_mut().event_rx.try_recv() {
|
||||
events.push(event);
|
||||
}
|
||||
for event in events {
|
||||
self.process_event(event)?;
|
||||
// Drain and process any events (including PreviewReady)
|
||||
self.tick()?;
|
||||
|
||||
// Exit as soon as the thread is done and we have processed its events
|
||||
let finished = self
|
||||
.skim
|
||||
.app()
|
||||
.preview
|
||||
.thread_handle
|
||||
.as_ref()
|
||||
.map(|h| h.is_finished())
|
||||
.unwrap_or(true);
|
||||
if finished {
|
||||
// One final drain to catch any events emitted right at thread exit
|
||||
self.tick()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
|
|
|
|||
Loading…
Reference in a new issue