1#![warn(missing_docs)]
26
27#[macro_use]
28extern crate log;
29
30use std::any::Any;
31use std::borrow::Cow;
32use std::env;
33use std::fmt::Display;
34use std::sync::Arc;
35use std::sync::atomic::AtomicBool;
36use std::time::{Duration, Instant};
37
38use color_eyre::eyre::Result;
39use color_eyre::eyre::{self, OptionExt};
40use crossterm::event::KeyCode;
41use crossterm::event::KeyEvent;
42use crossterm::event::KeyModifiers;
43use ratatui::prelude::CrosstermBackend;
44use ratatui::style::Style;
45use ratatui::text::{Line, Span};
46use reader::Reader;
47use tokio::select;
48use tokio::sync::mpsc::error::TryRecvError;
49use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
50use tui::App;
51use tui::Event;
52use tui::Size;
53
54pub use crate::engine::fuzzy::FuzzyAlgorithm;
55pub use crate::item::RankCriteria;
56pub use crate::options::SkimOptions;
57pub use crate::output::SkimOutput;
58pub use crate::skim_item::SkimItem;
59use crate::tui::event::Action;
60
61pub mod binds;
62mod engine;
63pub mod field;
64pub mod fuzzy_matcher;
65pub mod helper;
66pub mod item;
67pub mod matcher;
68pub mod options;
69mod output;
70pub mod prelude;
71pub mod reader;
72mod skim_item;
73pub mod spinlock;
74pub mod theme;
75pub mod tmux;
76pub mod tui;
77mod util;
78
79#[cfg(feature = "cli")]
80pub mod completions;
81#[cfg(feature = "cli")]
82pub mod manpage;
83
84pub trait AsAny {
87 fn as_any(&self) -> &dyn Any;
89 fn as_any_mut(&mut self) -> &mut dyn Any;
91}
92
93impl<T: Any> AsAny for T {
94 fn as_any(&self) -> &dyn Any {
95 self
96 }
97
98 fn as_any_mut(&mut self) -> &mut dyn Any {
99 self
100 }
101}
102
103#[derive(Default, Debug, Clone)]
106pub enum Matches {
108 #[default]
110 None,
111 CharIndices(Vec<usize>),
113 CharRange(usize, usize),
115 ByteRange(usize, usize),
117}
118
119#[derive(Default, Clone)]
120pub struct DisplayContext {
122 pub score: i32,
124 pub matches: Matches,
126 pub container_width: usize,
128 pub base_style: Style,
130 pub matched_syle: Style,
132}
133
134impl DisplayContext {
135 pub fn to_line(self, cow: Cow<str>) -> Line {
137 let text: String = cow.into_owned();
138
139 match &self.matches {
142 Matches::CharIndices(indices) => {
143 let mut res = Line::default();
144 let mut chars = text.chars();
145 let mut prev_index = 0;
146 for &index in indices {
147 let span_content = chars.by_ref().take(index - prev_index);
148 res.push_span(Span::styled(span_content.collect::<String>(), self.base_style));
149 let highlighted_char = chars.next().unwrap_or_default().to_string();
150
151 res.push_span(Span::styled(highlighted_char, self.base_style.patch(self.matched_syle)));
152 prev_index = index + 1;
153 }
154 res.push_span(Span::styled(chars.collect::<String>(), self.base_style));
155 res
156 }
157 #[allow(clippy::cast_possible_truncation)]
159 Matches::CharRange(start, end) => {
160 let mut chars = text.chars();
161 let mut res = Line::default();
162 res.push_span(Span::styled(
163 chars.by_ref().take(*start).collect::<String>(),
164 self.base_style,
165 ));
166 let highlighted_text = chars.by_ref().take(*end - *start).collect::<String>();
167
168 res.push_span(Span::styled(highlighted_text, self.base_style.patch(self.matched_syle)));
169 res.push_span(Span::styled(chars.collect::<String>(), self.base_style));
170 res
171 }
172 Matches::ByteRange(start, end) => {
173 let mut bytes = text.bytes();
174 let mut res = Line::default();
175 res.push_span(Span::styled(
176 String::from_utf8(bytes.by_ref().take(*start).collect()).unwrap(),
177 self.base_style,
178 ));
179 let highlighted_bytes = bytes.by_ref().take(*end - *start).collect();
180 let highlighted_text = String::from_utf8(highlighted_bytes).unwrap();
181
182 res.push_span(Span::styled(highlighted_text, self.base_style.patch(self.matched_syle)));
183 res.push_span(Span::styled(
184 String::from_utf8(bytes.collect()).unwrap(),
185 self.base_style,
186 ));
187 res
188 }
189 Matches::None => Line::from(vec![Span::styled(text, self.base_style)]),
190 }
191 }
192}
193
194pub struct PreviewContext<'a> {
199 pub query: &'a str,
201 pub cmd_query: &'a str,
203 pub width: usize,
205 pub height: usize,
207 pub current_index: usize,
209 pub current_selection: &'a str,
211 pub selected_indices: &'a [usize],
213 pub selections: &'a [&'a str],
215}
216
217#[derive(Default, Copy, Clone, Debug)]
220pub struct PreviewPosition {
222 pub h_scroll: Size,
224 pub h_offset: Size,
226 pub v_scroll: Size,
228 pub v_offset: Size,
230}
231
232pub enum ItemPreview {
234 Command(String),
236 Text(String),
238 AnsiText(String),
240 CommandWithPos(String, PreviewPosition),
242 TextWithPos(String, PreviewPosition),
244 AnsiWithPos(String, PreviewPosition),
246 Global,
248}
249
250#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)]
254#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
255#[cfg_attr(feature = "cli", clap(rename_all = "snake_case"))]
256pub enum CaseMatching {
258 Respect,
260 Ignore,
262 #[default]
264 Smart,
265}
266
267#[derive(PartialEq, Eq, Clone, Debug)]
268#[allow(dead_code)]
269pub enum MatchRange {
271 ByteRange(usize, usize),
273 Chars(Vec<usize>),
275}
276
277pub type Rank = [i32; 5];
279
280#[derive(Clone)]
281pub struct MatchResult {
283 pub rank: Rank,
285 pub matched_range: MatchRange,
287}
288
289impl MatchResult {
290 #[must_use]
291 pub fn range_char_indices(&self, text: &str) -> Vec<usize> {
293 match &self.matched_range {
294 &MatchRange::ByteRange(start, end) => {
295 let first = text[..start].chars().count();
296 let last = first + text[start..end].chars().count();
297 (first..last).collect()
298 }
299 MatchRange::Chars(vec) => vec.clone(),
300 }
301 }
302}
303
304pub trait MatchEngine: Sync + Send + Display {
306 fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult>;
308}
309
310pub trait MatchEngineFactory {
312 fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine>;
314 fn create_engine(&self, query: &str) -> Box<dyn MatchEngine> {
316 self.create_engine_with_case(query, CaseMatching::default())
317 }
318}
319
320pub trait Selector {
325 fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool;
327}
328
329pub type SkimItemSender = UnboundedSender<Arc<dyn SkimItem>>;
332pub type SkimItemReceiver = UnboundedReceiver<Arc<dyn SkimItem>>;
334
335pub struct Skim {}
337
338impl Skim {
339 pub fn run_with(options: SkimOptions, source: Option<SkimItemReceiver>) -> Result<SkimOutput> {
354 let height = Size::try_from(options.height.as_str())?;
355
356 let theme = Arc::new(crate::theme::ColorTheme::init_from_options(&options));
359 let mut reader = Reader::from_options(&options).source(source);
360 const SKIM_DEFAULT_COMMAND: &str = "find .";
361 let default_command = String::from(match env::var("SKIM_DEFAULT_COMMAND").as_deref() {
362 Err(_) | Ok("") => SKIM_DEFAULT_COMMAND,
363 Ok(v) => v,
364 });
365 let cmd = options.cmd.clone().unwrap_or(default_command);
366 let listen_socket = options.listen.clone();
367
368 let mut app = App::from_options(options, theme.clone(), cmd.clone());
369
370 let rt = tokio::runtime::Runtime::new()?;
371 let mut final_event: Event = Event::Quit;
372 let mut final_key: KeyEvent = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
373 rt.block_on(async {
374 let initial_cmd = if app.options.interactive && app.options.cmd.is_some() {
378 let expanded = app.expand_cmd(&cmd, true);
379 log::debug!(
380 "Interactive mode: initial_cmd = {:?} (from template {:?})",
381 expanded,
382 cmd
383 );
384 expanded
385 } else {
386 cmd.clone()
387 };
388 log::debug!("Starting reader with initial_cmd: {:?}", initial_cmd);
389 let (item_tx, mut item_rx) = unbounded_channel();
390 let mut reader_control = reader.run(item_tx.clone(), &initial_cmd);
391
392 let mut matcher_interval = tokio::time::interval(Duration::from_millis(100));
396 let reader_done = Arc::new(AtomicBool::new(false));
397 let reader_done_clone = reader_done.clone();
398 let (reader_interrupt_tx, mut reader_interrupt_rx) = unbounded_channel();
399
400 let item_pool = app.item_pool.clone();
401 tokio::spawn(async move {
402 const BATCH: usize = 4096; loop {
404 if reader_interrupt_rx
405 .try_recv()
406 .unwrap_or_else(|e| e == TryRecvError::Disconnected)
407 {
408 debug!("stopping reader receiver thread");
409 break;
410 }
411 let mut buf = Vec::with_capacity(BATCH);
412 trace!("getting items");
413 if item_rx.recv_many(&mut buf, BATCH).await > 0 {
414 item_pool.append(buf);
415 trace!("Got new items, len {}", item_pool.len());
416 } else {
417 reader_done_clone.store(true, std::sync::atomic::Ordering::Relaxed);
418 }
419 }
420 });
421
422 app.restart_matcher(true);
424
425 let min_items_before_enter = if app.options.exit_0 {
427 1
428 } else if app.options.select_1 {
429 2
430 } else {
431 0
432 };
433
434 let mut should_enter = true;
435 if min_items_before_enter > 0 {
436 while app.matcher_control.get_num_matched() < min_items_before_enter
437 && !app.matcher_control.stopped()
438 && !reader_done.load(std::sync::atomic::Ordering::Relaxed) {
439 let curr = app.matcher_control.get_num_matched();
440 tokio::time::sleep(Duration::from_millis(10)).await;
441 trace!(
442 "waiting for matcher, stopped: {}, processed: {}, pool: {}, matched: {}, query: {}, reader_done: {}, reader_control_done: {}",
443 app.matcher_control.stopped(),
444 app.matcher_control.get_num_processed(),
445 app.item_pool.num_not_taken(),
446 curr,
447 app.input.value,
448 reader_done.load(std::sync::atomic::Ordering::Relaxed),
449 reader_control.is_done()
450 );
451 app.restart_matcher(false);
452 };
453 trace!("checking for matched item count before entering: {}/{min_items_before_enter}", app.matcher_control.get_num_matched());
454 if app.matcher_control.get_num_matched() == min_items_before_enter - 1
455 {
456 app.item_list.items = app.item_list.processed_items.lock().take().unwrap_or_default().items;
457 debug!("early exit, result: {:?}", app.results());
458 should_enter = false;
459 final_event = Event::Action(Action::Accept(None));
460 }
461 }
462
463 if should_enter {
464 let listener = if let Some(socket_name) = &listen_socket {
465 debug!("starting listener on socket at {socket_name}");
466 Some(
467 interprocess::local_socket::ListenerOptions::new()
468 .name(
469 interprocess::local_socket::ToNsName::to_ns_name::<
470 interprocess::local_socket::GenericNamespaced,
471 >(socket_name.as_str())
472 .unwrap_or_else(|e| {
473 panic!("Failed to build full name for IPC listener from {socket_name}: {e}")
474 }),
475 )
476 .create_tokio()
477 .unwrap_or_else(|e| panic!("Failed to create tokio IPC listener at {socket_name}: {e}")),
478 )
479 } else {
480 None
481 };
482 let backend = CrosstermBackend::new(std::io::BufWriter::new(std::io::stderr()));
483 let mut tui = tui::Tui::new_with_height(backend, height)?;
484 let event_tx_clone = tui.event_tx.clone();
485 tui.enter()?;
486 loop {
487 select! {
488 event = tui.next() => {
489 let evt = event.ok_or_eyre("Could not acquire next event")?;
490
491 if let Event::Reload(new_cmd) = &evt {
493 debug!("reloading with cmd {new_cmd}");
494 reader_control.kill();
496 app.item_pool.clear();
498 if !app.options.no_clear_if_empty {
501 app.item_list.clear();
502 }
503 app.restart_matcher(true);
504 reader_control = reader.run(item_tx.clone(), new_cmd);
506 reader_done.store(false, std::sync::atomic::Ordering::Relaxed);
507 }
508 if let Event::Key(k) = &evt {
509 final_key = k.to_owned();
510 } else {
511 final_event = evt.to_owned();
512 }
513
514 if !reader_control.is_done() {
516 app.reader_timer = Instant::now();
517 } else if ! reader_done.load(std::sync::atomic::Ordering::Relaxed) {
518 reader_done.store(true, std::sync::atomic::Ordering::Relaxed);
519 app.restart_matcher(true);
520 }
521 app.handle_event(&mut tui, &evt)?;
522 }
523 _ = matcher_interval.tick() => {
524 app.restart_matcher(false);
525 }
526 Ok(stream) = async {
527 match &listener {
528 Some(l) => interprocess::local_socket::traits::tokio::Listener::accept(l).await,
529 None => std::future::pending().await,
530 }
531 } => {
532 let event_tx_clone_ipc = event_tx_clone.clone();
533 tokio::spawn(async move {
534 use tokio::io::AsyncBufReadExt;
535 let reader = tokio::io::BufReader::new(stream);
536 let mut lines = reader.lines();
537 while let Ok(Some(line)) = lines.next_line().await {
538 debug!("listener: got {line}");
539 if let Ok(act) = ron::from_str::<Action>(&line) {
540 debug!("listener: parsed into action {act:?}");
541 _ = event_tx_clone_ipc.send(Event::Action(act));
542 _ = event_tx_clone_ipc.send(Event::Render);
543 }
544 }
545 });
546 }
547 }
548
549 if app.should_quit {
550 break;
551 }
552 }
553 }
554 reader_control.kill();
555 let _ = reader_interrupt_tx.send(true);
556 eyre::Ok(())
557 })?;
558
559 let is_abort = !matches!(&final_event, Event::Action(Action::Accept(_)));
561
562 Ok(SkimOutput {
563 cmd: if app.options.interactive {
564 app.input.to_string()
566 } else if app.options.cmd_query.is_some() {
567 app.options.cmd_query.clone().unwrap()
569 } else {
570 cmd
572 },
573 final_event,
574 final_key,
575 query: app.input.to_string(),
576 is_abort,
577 selected_items: app.results(),
578 header: app.header.header,
579 })
580 }
581}