Skip to main content

skim/
lib.rs

1//! Skim is a fuzzy finder library for Rust.
2//!
3//! It provides a fast and customizable way to filter and select items interactively,
4//! similar to fzf. Skim can be used as a library or as a command-line tool.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use skim::prelude::*;
10//!
11//! let options = SkimOptionsBuilder::default()
12//!     .height("50%")
13//!     .multi(true)
14//!     .build()
15//!     .unwrap();
16//!
17//! let output = Skim::run_items(
18//!         options,
19//!         ["awk", "bash", "csh", "dash", "fish", "ksh", "zsh"]
20//!     ).unwrap();
21//! ```
22
23#[macro_use]
24extern crate log;
25
26#[global_allocator]
27static GLOBAL_ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
28
29use std::any::Any;
30use std::borrow::Cow;
31use std::fmt::Display;
32use std::process::Command;
33use std::sync::Arc;
34
35use crate::fuzzy_matcher::MatchIndices;
36use ratatui::{
37    style::Style,
38    text::{Line, Span},
39};
40
41pub use crate::engine::fuzzy::FuzzyAlgorithm;
42pub use crate::item::RankCriteria;
43pub use crate::options::SkimOptions;
44pub use crate::output::SkimOutput;
45pub use crate::skim::*;
46pub use crate::skim_item::SkimItem;
47use crate::tui::Size;
48pub use util::printf;
49
50pub mod binds;
51mod engine;
52pub mod field;
53pub mod fuzzy_matcher;
54pub mod helper;
55pub mod item;
56pub mod matcher;
57pub mod options;
58mod output;
59#[cfg(unix)]
60pub mod popup;
61pub mod prelude;
62pub mod reader;
63mod skim;
64mod skim_item;
65pub mod spinlock;
66pub mod theme;
67pub mod thread_pool;
68pub mod tui;
69mod util;
70
71#[cfg(feature = "cli")]
72pub mod manpage;
73#[cfg(feature = "cli")]
74pub mod shell;
75
76#[cfg(unix)]
77const SKIM_DEFAULT_COMMAND: &str = "find .";
78#[cfg(windows)]
79const SKIM_DEFAULT_COMMAND: &str = "dir /s /b /A:-D";
80
81#[cfg(unix)]
82fn shell_cmd(cmd: &str) -> Command {
83    let mut c = Command::new("sh");
84    c.arg("-c").arg(cmd);
85    c
86}
87#[cfg(windows)]
88fn shell_cmd(cmd: &str) -> Command {
89    use std::os::windows::process::CommandExt as _;
90    // `cmd.exe` does not parse its command line using MSVC rules, so the default
91    // `Command::arg` escaping (quoting/backslash-escaping) corrupts shell
92    // metacharacters like `|`, `&`, `>` and embedded quotes. Pass the command
93    // string verbatim via `raw_arg` so cmd.exe sees exactly what the user wrote.
94    let mut c = Command::new("cmd");
95    c.arg("/c").raw_arg(cmd);
96    c
97}
98
99//------------------------------------------------------------------------------
100/// Trait for downcasting to concrete types from trait objects
101pub trait AsAny {
102    /// Returns a reference to the value as `Any`
103    fn as_any(&self) -> &dyn Any;
104    /// Returns a mutable reference to the value as `Any`
105    fn as_any_mut(&mut self) -> &mut dyn Any;
106}
107
108impl<T: Any> AsAny for T {
109    fn as_any(&self) -> &dyn Any {
110        self
111    }
112
113    fn as_any_mut(&mut self) -> &mut dyn Any {
114        self
115    }
116}
117
118//------------------------------------------------------------------------------
119// Display Context
120#[derive(Default, Debug, Clone)]
121/// Represents how a query matches an item
122pub enum Matches {
123    /// No matches
124    #[default]
125    None,
126    /// Matches at specific character indices
127    CharIndices(MatchIndices),
128    /// Matches in a character range (start, end)
129    CharRange(usize, usize),
130    /// Matches in a byte range (start, end)
131    ByteRange(usize, usize),
132}
133
134#[derive(Default, Clone)]
135/// Context information for displaying an item
136pub struct DisplayContext {
137    /// The match score for this item
138    pub score: i32,
139    /// Where the query matched in the item
140    pub matches: Matches,
141    /// The width of the container to display in
142    pub container_width: usize,
143    /// The base style to apply to non-matched portions
144    pub base_style: Style,
145    /// The style to apply to matched portions
146    pub matched_style: Style,
147}
148
149impl DisplayContext {
150    /// Converts the context and text into a styled `Line` with highlighted matches
151    ///
152    /// # Panics
153    ///
154    /// Panics if the byte ranges in `Matches::ByteRange` do not align with valid UTF-8 boundaries.
155    #[must_use]
156    pub fn to_line(self, cow: Cow<str>) -> Line {
157        let text: String = cow.into_owned();
158
159        // Combine base_style with match style for highlighted text
160        // Match style takes precedence for fg, but inherits bg from base if not set
161        match &self.matches {
162            Matches::CharIndices(indices) => {
163                let mut res = Line::default();
164                let mut chars = text.chars();
165                let mut prev_index = 0;
166                for &index in indices {
167                    let span_content = chars.by_ref().take(index - prev_index);
168                    res.push_span(Span::styled(span_content.collect::<String>(), self.base_style));
169                    let highlighted_char = chars.next().unwrap_or_default().to_string();
170
171                    res.push_span(Span::styled(
172                        highlighted_char,
173                        self.base_style.patch(self.matched_style),
174                    ));
175                    prev_index = index + 1;
176                }
177                res.push_span(Span::styled(chars.collect::<String>(), self.base_style));
178                res
179            }
180            // AnsiString::from((context.text, indices, context.highlight_attr)),
181            #[allow(clippy::cast_possible_truncation)]
182            Matches::CharRange(start, end) => {
183                let mut chars = text.chars();
184                let mut res = Line::default();
185                res.push_span(Span::styled(
186                    chars.by_ref().take(*start).collect::<String>(),
187                    self.base_style,
188                ));
189                let highlighted_text = chars.by_ref().take(*end - *start).collect::<String>();
190
191                res.push_span(Span::styled(
192                    highlighted_text,
193                    self.base_style.patch(self.matched_style),
194                ));
195                res.push_span(Span::styled(chars.collect::<String>(), self.base_style));
196                res
197            }
198            Matches::ByteRange(start, end) => {
199                let mut bytes = text.bytes();
200                let mut res = Line::default();
201                res.push_span(Span::styled(
202                    String::from_utf8(bytes.by_ref().take(*start).collect()).unwrap(),
203                    self.base_style,
204                ));
205                let highlighted_bytes = bytes.by_ref().take(*end - *start).collect();
206                let highlighted_text = String::from_utf8(highlighted_bytes).unwrap();
207
208                res.push_span(Span::styled(
209                    highlighted_text,
210                    self.base_style.patch(self.matched_style),
211                ));
212                res.push_span(Span::styled(
213                    String::from_utf8(bytes.collect()).unwrap(),
214                    self.base_style,
215                ));
216                res
217            }
218            Matches::None => Line::from(vec![Span::styled(text, self.base_style)]),
219        }
220    }
221}
222
223//------------------------------------------------------------------------------
224// Preview Context
225
226/// Context information for generating item previews
227pub struct PreviewContext<'a> {
228    /// The current search query
229    pub query: &'a str,
230    /// The current command query (for interactive mode)
231    pub cmd_query: &'a str,
232    /// Width of the preview window
233    pub width: usize,
234    /// Height of the preview window
235    pub height: usize,
236    /// Index of the current item
237    pub current_index: usize,
238    /// Text of the current selection
239    pub current_selection: &'a str,
240    /// selected item indices (may or may not include current item)
241    pub selected_indices: &'a [usize],
242    /// selected item texts (may or may not include current item)
243    pub selections: &'a [&'a str],
244}
245
246//------------------------------------------------------------------------------
247// Preview
248
249/// Position and scroll information for preview display
250#[derive(Default, Copy, Clone, Debug)]
251pub struct PreviewPosition {
252    /// Horizontal scroll position
253    pub h_scroll: Size,
254    /// Horizontal offset
255    pub h_offset: Size,
256    /// Vertical scroll position
257    pub v_scroll: Size,
258    /// Vertical offset
259    pub v_offset: Size,
260}
261
262/// Defines how an item should be previewed
263pub enum ItemPreview {
264    /// execute the command and print the command's output
265    Command(String),
266    /// Display the prepared text(lines)
267    Text(String),
268    /// Display the colored text(lines)
269    AnsiText(String),
270    /// Execute a command and display output with position
271    CommandWithPos(String, PreviewPosition),
272    /// Display text with position
273    TextWithPos(String, PreviewPosition),
274    /// Display ANSI-colored text with position
275    AnsiWithPos(String, PreviewPosition),
276    /// Use global command settings to preview the item
277    Global,
278}
279
280//==============================================================================
281// A match engine will execute the matching algorithm
282
283/// Case sensitivity mode for matching
284#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)]
285#[cfg_attr(feature = "cli", derive(clap::ValueEnum), clap(rename_all = "snake_case"))]
286pub enum CaseMatching {
287    /// Case-sensitive matching
288    Respect,
289    /// Case-insensitive matching
290    Ignore,
291    /// Smart case: case-insensitive unless query contains uppercase
292    #[default]
293    Smart,
294}
295
296/// Typo tolerance configuration for fuzzy matching
297///
298/// Controls how many character mismatches (typos) are allowed when matching.
299#[derive(Eq, PartialEq, Debug, Copy, Clone, Default)]
300pub enum Typos {
301    /// No typo tolerance — query must match exactly
302    #[default]
303    Disabled,
304    /// Adaptive typo tolerance — allows `pattern_length / 4` typos
305    Smart,
306    /// Fixed typo tolerance — allows exactly `n` typos
307    Fixed(usize),
308}
309
310impl From<usize> for Typos {
311    fn from(n: usize) -> Self {
312        match n {
313            0 => Typos::Disabled,
314            n => Typos::Fixed(n),
315        }
316    }
317}
318
319/// Represents the range of a match in an item
320#[derive(PartialEq, Eq, Clone, Debug)]
321pub enum MatchRange {
322    /// Range of bytes (start, end)
323    ByteRange(usize, usize),
324    /// Range of character indices (start, end) — used by fuzzy matchers that
325    /// operate on `char` arrays rather than raw bytes.
326    CharRange(usize, usize),
327    /// Individual character indices that matched
328    Chars(MatchIndices),
329}
330
331/// Rank stores the raw match measurements used for sorting results.
332///
333/// Named fields preserve the semantic meaning of each value. The actual
334/// sort key (taking into account the user-configured tiebreak criteria and
335/// their direction) is computed lazily via [`Rank::sort_key`] rather than
336/// being baked in at construction time.
337#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
338pub struct Rank {
339    /// Raw fuzzy/exact match score (higher is a better match)
340    pub score: i32,
341    /// Index of the first matched character (0-based)
342    pub begin: i32,
343    /// Index of the last matched character (0-based)
344    pub end: i32,
345    /// Length of the item text in bytes
346    pub length: i32,
347    /// Ordinal position of the item in the input stream
348    pub index: i32,
349    /// Byte offset of the first character after the last path separator (`/` or `\`).
350    /// Equal to `0` when the item text contains no path separator.
351    pub path_name_offset: i32,
352}
353
354/// Result of matching a query against an item
355#[derive(Clone, Debug)]
356pub struct MatchResult {
357    /// The rank/score of this match
358    pub rank: Rank,
359    /// The range where the match occurred
360    pub matched_range: MatchRange,
361}
362
363impl MatchResult {
364    #[must_use]
365    /// Converts the match range to character indices
366    pub fn range_char_indices(&self, text: &str) -> MatchIndices {
367        match &self.matched_range {
368            &MatchRange::ByteRange(start, end) => {
369                let first = text[..start].chars().count();
370                let last = first + text[start..end].chars().count();
371                (first..last).collect()
372            }
373            &MatchRange::CharRange(start, end) => (start..end).collect(),
374            MatchRange::Chars(vec) => vec.clone(),
375        }
376    }
377}
378
379/// A matching engine that can match queries against items
380pub trait MatchEngine: Sync + Send + Display {
381    /// Matches an item against the query, returning a result if matched
382    fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult>;
383}
384
385/// Factory for creating match engines
386pub trait MatchEngineFactory {
387    /// Creates a match engine with explicit case sensitivity
388    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine>;
389    /// Creates a match engine with default case sensitivity
390    fn create_engine(&self, query: &str) -> Box<dyn MatchEngine> {
391        self.create_engine_with_case(query, CaseMatching::default())
392    }
393}
394
395impl MatchEngineFactory for Box<dyn MatchEngineFactory> {
396    fn create_engine_with_case(&self, query: &str, case: CaseMatching) -> Box<dyn MatchEngine> {
397        (**self).create_engine_with_case(query, case)
398    }
399}
400
401//------------------------------------------------------------------------------
402// Preselection
403
404/// A selector that determines whether an item should be "pre-selected" in multi-selection mode
405pub trait Selector {
406    /// Returns true if the item at the given index should be pre-selected
407    fn should_select(&self, index: usize, item: &dyn SkimItem) -> bool;
408}
409
410//------------------------------------------------------------------------------
411/// Sender for streaming items to skim
412pub type SkimItemSender = kanal::Sender<Vec<Arc<dyn SkimItem>>>;
413/// Receiver for streaming items to skim
414pub type SkimItemReceiver = kanal::Receiver<Vec<Arc<dyn SkimItem>>>;